├── .eslintignore ├── .github ├── CODEOWNERS ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .vscode ├── settings.json └── launch.json ├── .arcconfig ├── .gitignore ├── packages ├── ui │ ├── src │ │ ├── public │ │ │ ├── favicon.ico │ │ │ ├── manifest.json │ │ │ └── index.html │ │ ├── View │ │ │ ├── Tags │ │ │ │ ├── headers.js │ │ │ │ ├── queries.js │ │ │ │ └── TagModal.jsx │ │ │ ├── statusEnum.js │ │ │ ├── Panels │ │ │ │ ├── stackSelectionHeaders.js │ │ │ │ ├── panelsHeaders.js │ │ │ │ ├── PanelWrapper.jsx │ │ │ │ └── queries.js │ │ │ ├── Realms │ │ │ │ ├── headers.js │ │ │ │ └── realmQueries.js │ │ │ ├── Stacks │ │ │ │ ├── stacksHeaders.js │ │ │ │ ├── StackWrapper.jsx │ │ │ │ ├── ActionParameter.jsx │ │ │ │ └── queries.js │ │ │ ├── Devices │ │ │ │ ├── headers.js │ │ │ │ ├── DeviceWrapper.jsx │ │ │ │ ├── queries.js │ │ │ │ └── ProviderTile.jsx │ │ │ ├── Controllers │ │ │ │ ├── headers.js │ │ │ │ ├── ControllerWrapper.jsx │ │ │ │ └── queries.js │ │ │ ├── components │ │ │ │ ├── Padding.jsx │ │ │ │ ├── ModalStateManager.jsx │ │ │ │ ├── NavGroup.jsx │ │ │ │ ├── SideNavLink.jsx │ │ │ │ ├── ReactError.jsx │ │ │ │ ├── Clock.jsx │ │ │ │ ├── arrayUtils.js │ │ │ │ ├── GraphQLError.jsx │ │ │ │ ├── SidebarNav.jsx │ │ │ │ └── DeleteObjectModal.jsx │ │ │ ├── hooks │ │ │ │ └── useWindowDimensions.js │ │ │ ├── Executer │ │ │ │ ├── queries.js │ │ │ │ ├── DeviceTile.jsx │ │ │ │ └── undraw_No_data_re_kwbl.svg │ │ │ ├── Shotbox │ │ │ │ ├── queries.js │ │ │ │ ├── ShotboxPanelWrapper.jsx │ │ │ │ ├── ShotboxPanel.jsx │ │ │ │ └── ShotboxControllerWrapper.jsx │ │ │ ├── Dashboard │ │ │ │ ├── Status.jsx │ │ │ │ ├── Notes.jsx │ │ │ │ ├── ResourceSummary.jsx │ │ │ │ └── Dashboard.jsx │ │ │ ├── Landing │ │ │ │ └── Landing.jsx │ │ │ └── index.scss │ │ ├── globalContext.js │ │ ├── index.js │ │ ├── theme.scss │ │ └── App.js │ ├── .babelrc │ ├── readme.md │ ├── nginx.conf │ ├── Dockerfile │ ├── package.json │ └── webpack.config.js ├── link │ ├── src │ │ ├── network │ │ │ ├── ui │ │ │ │ ├── public │ │ │ │ │ ├── favicon.ico │ │ │ │ │ └── index.html │ │ │ │ └── src │ │ │ │ │ ├── globalContext.js │ │ │ │ │ ├── index.scss │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── GraphQLError.jsx │ │ │ ├── express.js │ │ │ ├── schema.js │ │ │ └── graphql.js │ │ ├── utils │ │ │ ├── statusEnum.js │ │ │ ├── regexEnum.js │ │ │ ├── omitDeep.js │ │ │ ├── config.js │ │ │ └── log.js │ │ ├── link.js │ │ └── streamdeck │ │ │ ├── utils │ │ │ ├── writePanel.js │ │ │ ├── handleButtonPress.js │ │ │ └── buttonLUT.js │ │ │ ├── graphics │ │ │ └── index.js │ │ │ └── queries.js │ ├── copy-deps.sh │ ├── .babelrc │ ├── readme.md │ ├── copy-builds.sh │ ├── webpack.config.js │ └── package.json ├── core │ ├── src │ │ ├── network │ │ │ ├── index.js │ │ │ ├── graphql │ │ │ │ ├── schema.js │ │ │ │ ├── coreTypes │ │ │ │ │ ├── globalColourType.js │ │ │ │ │ ├── globalColourInputType.js │ │ │ │ │ ├── logType.js │ │ │ │ │ ├── realmInputType.js │ │ │ │ │ ├── coreType.js │ │ │ │ │ ├── coreInputType.js │ │ │ │ │ └── realmType.js │ │ │ │ ├── providerTypes │ │ │ │ │ ├── providerFunctionType.js │ │ │ │ │ └── providerType.js │ │ │ │ ├── tagTypes │ │ │ │ │ ├── tagType.js │ │ │ │ │ └── tagInputType.js │ │ │ │ ├── controllerTypes │ │ │ │ │ ├── controllerLayoutType.js │ │ │ │ │ ├── controllerType.js │ │ │ │ │ └── controllerInputType.js │ │ │ │ ├── bridgeTypes │ │ │ │ │ ├── bridgeUpdateInputType.js │ │ │ │ │ └── bridgeType.js │ │ │ │ ├── parameterTypes │ │ │ │ │ └── parameterType.js │ │ │ │ ├── subscriptions.js │ │ │ │ ├── deviceTypes │ │ │ │ │ ├── deviceUpdateInputType.js │ │ │ │ │ └── deviceType.js │ │ │ │ ├── panelTypes │ │ │ │ │ ├── panelType.js │ │ │ │ │ └── panelUpdateInputType.js │ │ │ │ └── stackTypes │ │ │ │ │ ├── stackType.js │ │ │ │ │ └── stackUpdateInputType.js │ │ │ ├── apollo.js │ │ │ └── rosstalk.js │ │ ├── panels │ │ │ ├── index.js │ │ │ ├── getPanel.js │ │ │ ├── deletePanel.js │ │ │ ├── panelResolvers.js │ │ │ └── updatePanel.js │ │ ├── utils │ │ │ ├── statusEnum.js │ │ │ ├── regexEnum.js │ │ │ ├── actionTimeouts.js │ │ │ ├── omitDeep.js │ │ │ ├── log.js │ │ │ └── waterfall.js │ │ ├── controllers │ │ │ ├── controllerLayouts.js │ │ │ ├── deleteController.js │ │ │ ├── index.js │ │ │ ├── publishControllerUpdate.js │ │ │ ├── controllerResolvers.js │ │ │ ├── registerController.js │ │ │ └── updateController.js │ │ ├── bridges │ │ │ ├── bridgeResolvers.js │ │ │ └── index.js │ │ ├── tags │ │ │ ├── tagResolvers.js │ │ │ └── index.js │ │ ├── db.js │ │ ├── providers │ │ │ ├── protocolProviders │ │ │ │ ├── ProtocolProviderTCP.js │ │ │ │ ├── ProtocolProviderRossTalk.js │ │ │ │ └── ProtocolProviderOSC.js │ │ │ ├── connectionProviders │ │ │ │ ├── ConnectionProviderOSC.js │ │ │ │ ├── ConnectionProviderTCP.js │ │ │ │ └── ConnectionProviderSerial.js │ │ │ └── index.js │ │ ├── devices │ │ │ ├── deviceResolvers.js │ │ │ └── index.js │ │ ├── stacks │ │ │ ├── stackResolvers.js │ │ │ └── index.js │ │ └── main.js │ ├── .env │ ├── babel.config.js │ ├── Dockerfile │ ├── readme.md │ └── package.json └── shotbox │ ├── package.json │ └── readme.md ├── package.json ├── .eslintrc.js ├── docker-compose.canary.yml ├── docker-compose.achilles.yml ├── .gitmodules └── readme.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @monoxane -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } -------------------------------------------------------------------------------- /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "phabricator.uri" : "https://phabricator.boreal.systems/" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn-error.log 2 | node_modules 3 | build 4 | dist 5 | .DS_Store 6 | packages/core/db/* 7 | database/ 8 | logs.txt -------------------------------------------------------------------------------- /packages/ui/src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borealsystems/Director/HEAD/packages/ui/src/public/favicon.ico -------------------------------------------------------------------------------- /packages/link/src/network/ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borealsystems/Director/HEAD/packages/link/src/network/ui/public/favicon.ico -------------------------------------------------------------------------------- /packages/core/src/network/index.js: -------------------------------------------------------------------------------- 1 | import { initApollo } from './apollo' 2 | import { initRossTalk } from './rosstalk' 3 | 4 | export { initApollo, initRossTalk} -------------------------------------------------------------------------------- /packages/link/src/utils/statusEnum.js: -------------------------------------------------------------------------------- 1 | const STATUS = { 2 | UNKNOWN: -1, 3 | CLOSED: 0, 4 | OK: 1, 5 | CONNECTING: 2, 6 | ERROR: 3 7 | } 8 | 9 | export default STATUS 10 | -------------------------------------------------------------------------------- /packages/ui/src/View/Tags/headers.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | key: 'id', 4 | header: 'ID' 5 | }, 6 | { 7 | key: 'label', 8 | header: 'Name' 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /packages/core/src/panels/index.js: -------------------------------------------------------------------------------- 1 | import updatePanel from './updatePanel' 2 | import deletePanel from './deletePanel' 3 | import getPanel from './getPanel' 4 | 5 | export { updatePanel, deletePanel, getPanel } 6 | -------------------------------------------------------------------------------- /packages/link/src/network/ui/src/globalContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const globalContext = React.createContext({ 4 | theme: '', 5 | toggleTheme: () => {} 6 | }) 7 | 8 | export default globalContext 9 | -------------------------------------------------------------------------------- /packages/core/.env: -------------------------------------------------------------------------------- 1 | DIRECTOR_CORE_ID = DEV 2 | DIRECTOR_CORE_LABEL = Director Development 3 | DIRECTOR_CORE_DB_HOST = localhost 4 | DIRECTOR_CORE_DB_DATABASE = DirectorCore 5 | DIRECTOR_CORE_DB_USERNAME = Director 6 | DIRECTOR_CORE_DB_PASSWORD = S3CuR3P4ssw0rd -------------------------------------------------------------------------------- /packages/link/copy-deps.sh: -------------------------------------------------------------------------------- 1 | cp -r ../../node_modules/open/xdg-open dist/ 2 | cp -r ../../node_modules/node-hid/build/Release/HID.node dist/ 3 | cp -r ../../node_modules/skia-canvas/native/index.node dist/ 4 | cp -r ../../node_modules/fsevents/fsevents.node dist/ -------------------------------------------------------------------------------- /packages/ui/src/globalContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const globalContext = React.createContext({ 4 | realmContext: {}, 5 | setContextRealm: () => {}, 6 | theme: '', 7 | toggleTheme: () => {} 8 | }) 9 | 10 | export default globalContext 11 | -------------------------------------------------------------------------------- /packages/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import * as serviceWorker from './serviceWorker' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | 8 | serviceWorker.unregister() 9 | -------------------------------------------------------------------------------- /packages/ui/src/View/statusEnum.js: -------------------------------------------------------------------------------- 1 | const STATUS = { 2 | DISABLED: 'Disabled', 3 | UNKNOWN: 'UNKNOWN', 4 | CLOSED: 'Disconnected', 5 | OK: 'OK', 6 | CONNECTING: 'Connecting', 7 | ERROR: 'Error', 8 | WARNING: 'Warning' 9 | } 10 | 11 | export default STATUS 12 | -------------------------------------------------------------------------------- /packages/core/src/utils/statusEnum.js: -------------------------------------------------------------------------------- 1 | const STATUS = { 2 | DISABLED: 'Disabled', 3 | UNKNOWN: 'UNKNOWN', 4 | CLOSED: 'Disconnected', 5 | OK: 'OK', 6 | CONNECTING: 'Connecting', 7 | ERROR: 'Error', 8 | WARNING: 'Warning' 9 | } 10 | 11 | export default STATUS 12 | -------------------------------------------------------------------------------- /packages/ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": false }], 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-syntax-dynamic-import", 8 | "@babel/plugin-proposal-class-properties", 9 | "react-hot-loader/babel" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/ui/src/View/Panels/stackSelectionHeaders.js: -------------------------------------------------------------------------------- 1 | const headers = [ 2 | { 3 | key: 'id', 4 | header: 'ID' 5 | }, 6 | { 7 | key: 'label', 8 | header: 'Name' 9 | }, 10 | { 11 | key: 'panelLabel', 12 | header: 'Panel Label' 13 | } 14 | ] 15 | 16 | export default headers 17 | -------------------------------------------------------------------------------- /packages/link/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "node": "14.4.0" 9 | } 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-proposal-class-properties" 15 | ] 16 | } -------------------------------------------------------------------------------- /packages/core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: '14.4.0' 8 | } 9 | } 10 | ] 11 | ], 12 | plugins: [ 13 | [ 14 | '@babel/plugin-proposal-class-properties' 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/shotbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shotbox", 3 | "version": "0.0.0", 4 | "description": "Mobile and Desktop shotboxes for DirectorCore", 5 | "main": "src/main.js", 6 | "repository": "https://phabricator.boreal.systems/source/Director/", 7 | "author": "Oliver Herrmann ", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /packages/ui/src/View/Panels/panelsHeaders.js: -------------------------------------------------------------------------------- 1 | const headers = [ 2 | { 3 | key: 'id', 4 | header: 'ID' 5 | }, 6 | { 7 | key: 'label', 8 | header: 'Name' 9 | }, 10 | { 11 | key: 'description', 12 | header: 'Description' 13 | }, 14 | { 15 | key: 'size', 16 | header: 'Size' 17 | } 18 | ] 19 | 20 | export default headers 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "borealdirector", 3 | "version": "0.0.0-alpha", 4 | "repository": "https://github.com/borealsystems/Director.git", 5 | "author": "Oliver Herrmann ", 6 | "license": "GPL-3.0", 7 | "private": true, 8 | "workspaces": [ 9 | "packages/*", 10 | "packages/core/src/providers/deviceProviders/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/shotbox/readme.md: -------------------------------------------------------------------------------- 1 | # Boreal Systems Director Shotbox 2 | 3 | This package will consist of desktop, mobile, and web apps utilising React, React Native, and Electron, to let users trigger Stacks from a nice glossy softpanel. As you can see, it's existance has not eventuated yet. 4 | 5 | # Developement 6 | Don't. This is not ready to exist yet. 7 | 8 | # Deployment 9 | Don't. -------------------------------------------------------------------------------- /packages/ui/src/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Director", 3 | "name": "BorealDirector", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#161616" 15 | } -------------------------------------------------------------------------------- /packages/core/src/controllers/controllerLayouts.js: -------------------------------------------------------------------------------- 1 | const controllerLayouts = [ 2 | { id: 'elgato-streamdeck-mini', label: 'Elgato Streamdeck Mini', rows: 2, columns: 3 }, 3 | { id: 'elgato-streamdeck-original', label: 'Elgato Streamdeck', rows: 3, columns: 5 }, 4 | { id: 'elgato-streamdeck-xl', label: 'Elgato Streamdeck XL', rows: 4, columns: 8 } 5 | ] 6 | 7 | export default controllerLayouts 8 | -------------------------------------------------------------------------------- /packages/ui/src/View/Realms/headers.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | key: 'id', 4 | header: 'ID' 5 | }, 6 | { 7 | key: 'label', 8 | header: 'Name' 9 | }, 10 | { 11 | key: 'description', 12 | header: 'Description' 13 | }, 14 | { 15 | key: 'coreID', 16 | header: 'Core ID' 17 | }, 18 | { 19 | key: 'coreLabel', 20 | header: 'Core' 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/schema.js: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql' 2 | 3 | import { queries } from './queries' 4 | import { mutations } from './mutations' 5 | import { subscriptions, pubsub } from './subscriptions' 6 | 7 | var schema = new GraphQLSchema({ 8 | query: queries, 9 | mutation: mutations, 10 | subscription: subscriptions 11 | }) 12 | 13 | export { schema, pubsub } 14 | -------------------------------------------------------------------------------- /packages/core/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.15.3 2 | RUN apt-get update && apt-get install udev libusb-1.0-0-dev git cmake build-essential -y 3 | RUN git clone https://phabricator.boreal.systems/source/Director.git /borealsystems/director --recursive 4 | WORKDIR /borealsystems/director 5 | RUN yarn workspace core install 6 | RUN yarn workspace core run build 7 | 8 | EXPOSE 3000 9 | 10 | CMD yarn workspace core run prod -------------------------------------------------------------------------------- /packages/core/src/controllers/deleteController.js: -------------------------------------------------------------------------------- 1 | import {controllers} from '../db'; 2 | import log from '../utils/log'; 3 | import STATUS from '../utils/statusEnum'; 4 | 5 | const deleteController = id => new Promise(resolve => { 6 | controllers.deleteOne({ id: id }) 7 | log('info', 'core/controllers', `Deleted Controller ${id}`) 8 | resolve(STATUS.OK) 9 | }) 10 | 11 | export default deleteController 12 | -------------------------------------------------------------------------------- /packages/link/readme.md: -------------------------------------------------------------------------------- 1 | # Boreal Systems Director Link 2 | 3 | This package allows you to interface local USB controllers with a remote core over the network, it currently supports Elgato Streamdeck Minis (will support the rest when I have access to them again) 4 | 5 | # Developement 6 | Lots of yarn scripts, really bad code. 7 | 8 | # Deployment 9 | `yarn workspace link run release` will produce executable builds in ./dist -------------------------------------------------------------------------------- /packages/ui/readme.md: -------------------------------------------------------------------------------- 1 | # Boreal Systems Director UI 2 | 3 | This package is the UI for the Director Core that handles all the configuration interactions required to use Director, as well as a simple shotbox to facilitate testing of stacks. 4 | 5 | # Developement 6 | (From Director root) 7 | ``` 8 | yarn workspace ui 9 | yarn workspace ui run dev 10 | ``` 11 | # Deployment 12 | Don't. This is not production ready in any way whatsoever. -------------------------------------------------------------------------------- /packages/core/src/network/graphql/coreTypes/globalColourType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | } from 'graphql' 5 | 6 | const globalColourType = new GraphQLObjectType({ 7 | name: 'globalColourType', 8 | description: 'A Colour', 9 | fields: { 10 | id: { 11 | type: GraphQLString 12 | }, 13 | label: { 14 | type: GraphQLString 15 | } 16 | } 17 | }) 18 | 19 | export default globalColourType 20 | -------------------------------------------------------------------------------- /packages/ui/src/View/Stacks/stacksHeaders.js: -------------------------------------------------------------------------------- 1 | const headers = [ 2 | { 3 | key: 'id', 4 | header: 'ID' 5 | }, 6 | { 7 | key: 'label', 8 | header: 'Name' 9 | }, 10 | { 11 | key: 'panelLabel', 12 | header: 'Panel Label' 13 | }, 14 | { 15 | key: 'description', 16 | header: 'Description' 17 | }, 18 | { 19 | key: 'actionsCount', 20 | header: 'Actions' 21 | } 22 | ] 23 | 24 | export default headers 25 | -------------------------------------------------------------------------------- /packages/ui/src/theme.scss: -------------------------------------------------------------------------------- 1 | $feature-flags: ( 2 | enable-css-custom-properties: true, 3 | ); 4 | 5 | @import "@carbon/themes/scss/_mixins"; 6 | 7 | .dx--light { 8 | @include carbon--theme($carbon--theme--g10, true); 9 | background-color: #f4f4f4 !important; 10 | } 11 | 12 | .dx--dark { 13 | @include carbon--theme($carbon--theme--g100, true); 14 | background-color: #393939 !important; 15 | } 16 | 17 | @import "carbon-components/scss/globals/scss/styles"; -------------------------------------------------------------------------------- /packages/core/src/panels/getPanel.js: -------------------------------------------------------------------------------- 1 | import { panels, stacks } from '../db' 2 | 3 | const getPanel = id => new Promise((resolve, reject) => { 4 | panels.findOne({ id: id }) 5 | .then(panel => { 6 | panel.buttons.map(row => row.map(button => { 7 | button?.stack && (button.stack = stacks.findOne({id: button.stack.id})) 8 | })) 9 | resolve(panel) 10 | }) 11 | .catch(e => reject(e)) 12 | }) 13 | 14 | export default getPanel 15 | -------------------------------------------------------------------------------- /packages/core/src/controllers/index.js: -------------------------------------------------------------------------------- 1 | import registerController from './registerController' 2 | import updateController from './updateController' 3 | import publishControllerUpdate from './publishControllerUpdate' 4 | import controllerLayouts from './controllerLayouts' 5 | import deleteController from './deleteController' 6 | 7 | export { 8 | controllerLayouts, 9 | deleteController, 10 | publishControllerUpdate, 11 | registerController, 12 | updateController 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/coreTypes/globalColourInputType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLString, 4 | } from 'graphql' 5 | 6 | const globalColourInputType = new GraphQLInputObjectType({ 7 | name: 'globalColourInputType', 8 | description: 'A Colour', 9 | fields: { 10 | id: { 11 | type: GraphQLString 12 | }, 13 | label: { 14 | type: GraphQLString 15 | } 16 | } 17 | }) 18 | 19 | export default globalColourInputType 20 | -------------------------------------------------------------------------------- /packages/link/src/network/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | $feature-flags: ( 2 | enable-css-custom-properties: true, 3 | ); 4 | 5 | @import "@carbon/themes/scss/_mixins"; 6 | 7 | .dx--light { 8 | @include carbon--theme($carbon--theme--g10, true); 9 | background-color: #f4f4f4 !important; 10 | } 11 | 12 | .dx--dark { 13 | @include carbon--theme($carbon--theme--g100, true); 14 | background-color: #393939 !important; 15 | } 16 | 17 | @import "carbon-components/scss/globals/scss/styles"; -------------------------------------------------------------------------------- /packages/ui/src/View/Devices/headers.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | key: 'id', 4 | header: 'ID' 5 | }, 6 | { 7 | key: 'label', 8 | header: 'Name' 9 | }, 10 | { 11 | key: 'description', 12 | header: 'Description' 13 | }, 14 | { 15 | key: 'location', 16 | header: 'Location' 17 | }, 18 | { 19 | key: 'providerLabel', 20 | header: 'Provider' 21 | }, 22 | { 23 | key: 'status', 24 | header: 'Status' 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /packages/core/src/utils/regexEnum.js: -------------------------------------------------------------------------------- 1 | const REGEX = { 2 | HOST: '(\\b((25[0-5]|2[0-4]\\d|[01]?\\d{1,2}|\\*)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d{1,2}|\\*))|((([a-fA-F0-9]{1,4}|):){1,7}([a-fA-F0-9]{1,4}|:))|(^(?=^.{1,253}$)(([a-z\\d]([a-z\\d-]{0,62}[a-z\\d])*[.]){1,3}[a-z]{1,61})$)', 3 | PORT: '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$', 4 | SIGNEDINT: '^-?\\d+$', 5 | SIGNEDFLOAT: '([+-]?(?=\\.\\d|\\d)(?:\\d+)?(?:\\.?\\d*))(?:[eE]([+-]?\\d+))?' 6 | } 7 | 8 | export default REGEX 9 | -------------------------------------------------------------------------------- /packages/link/src/utils/regexEnum.js: -------------------------------------------------------------------------------- 1 | const REGEX = { 2 | HOST: '(^((?:([a-z0-9]\\.|[a-z0-9][a-z0-9\\-]{0,61}[a-z0-9])\\.)+)([a-z0-9]{2,63}|(?:[a-z0-9][a-z0-9\\-]{0,61}[a-z0-9]))\\.?$)|(\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\\.){3}(?:(?:2([0-4][0-9]|5 [0-5])|[0-1]?[0-9]?[0-9]))\\b)', 3 | PORT: '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$', 4 | SIGNEDINT: '^-?\\d+$', 5 | SIGNEDFLOAT: '([+-]?(?=\\.\\d|\\d)(?:\\d+)?(?:\\.?\\d*))(?:[eE]([+-]?\\d+))?' 6 | } 7 | 8 | export default REGEX 9 | -------------------------------------------------------------------------------- /packages/ui/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | 4 | location / { 5 | root /borealsystems/director/ui/public; 6 | index index.html; 7 | try_files $uri $uri/ /index.html; 8 | } 9 | 10 | location /graphql { 11 | proxy_pass http://core:3000; 12 | proxy_http_version 1.1; 13 | proxy_set_header Upgrade $http_upgrade; 14 | proxy_set_header Connection "Upgrade"; 15 | proxy_set_header Host $host; 16 | proxy_read_timeout 999999999; 17 | } 18 | } -------------------------------------------------------------------------------- /packages/core/src/utils/actionTimeouts.js: -------------------------------------------------------------------------------- 1 | class actionTimeouts { 2 | constructor (name) { 3 | this.name = name 4 | } 5 | 6 | static timeouts = []; 7 | static add (callback, timeout) { 8 | const id = setTimeout(() => { 9 | callback() 10 | }, timeout ?? 0) 11 | this.timeouts.push(id) 12 | } 13 | 14 | static clearAll () { 15 | let len = this.timeouts.length 16 | while (len > 0) { 17 | clearTimeout(this.timeouts.pop()) 18 | len-- 19 | } 20 | } 21 | } 22 | 23 | export { actionTimeouts } 24 | -------------------------------------------------------------------------------- /packages/ui/src/View/Controllers/headers.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | key: 'id', 4 | header: 'ID' 5 | }, 6 | { 7 | key: 'label', 8 | header: 'Name' 9 | }, 10 | { 11 | key: 'manufacturer', 12 | header: 'Manufacturer' 13 | }, 14 | { 15 | key: 'model', 16 | header: 'Model' 17 | }, 18 | { 19 | key: 'serial', 20 | header: 'Serial Number' 21 | }, 22 | { 23 | key: 'panelLabel', 24 | header: 'Panel' 25 | }, 26 | { 27 | key: 'status', 28 | header: 'Status' 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /packages/ui/src/View/components/Padding.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | function Padding ({ size }) { 5 | const spacingScale = [ 6 | '0.125rem', 7 | '0.25rem', 8 | '0.5rem', 9 | '0.75rem', 10 | '1rem', 11 | '1.5rem', 12 | '2rem', 13 | '2.5rem', 14 | '3rem' 15 | ] 16 | return   17 | } 18 | 19 | Padding.propTypes = { 20 | size: PropTypes.number 21 | } 22 | 23 | export default Padding 24 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/coreTypes/logType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString 4 | } from 'graphql' 5 | 6 | const logType = new GraphQLObjectType({ 7 | name: 'LogItem', 8 | fields: { 9 | id: { 10 | type: GraphQLString 11 | }, 12 | time: { 13 | type: GraphQLString 14 | }, 15 | level: { 16 | type: GraphQLString 17 | }, 18 | path: { 19 | type: GraphQLString 20 | }, 21 | message: { 22 | type: GraphQLString 23 | } 24 | } 25 | }) 26 | 27 | export default logType 28 | -------------------------------------------------------------------------------- /packages/core/src/bridges/bridgeResolvers.js: -------------------------------------------------------------------------------- 1 | import { registerBridge, bridges } from '.' 2 | 3 | const bridgeResolvers = { 4 | getBridgesQueryResolver: () => bridges, 5 | 6 | updateBridgeMutationResolver: (parent, args, context, info) => { 7 | var clientIP = context.req.headers['x-forwarded-for'] || context.req.connection.remoteAddress 8 | if (clientIP.substr(0, 7) === '::ffff:') { 9 | clientIP = clientIP.substr(7) 10 | } 11 | return registerBridge({ ...args.bridge, address: clientIP }) 12 | } 13 | } 14 | 15 | export { bridgeResolvers } 16 | -------------------------------------------------------------------------------- /packages/core/src/tags/tagResolvers.js: -------------------------------------------------------------------------------- 1 | import { tags } from '../db' 2 | import { updateTag, deleteTag } from '.' 3 | 4 | const tagResolvers = { 5 | tagsQueryResolver: async (p, args) => { 6 | const realmFilter = args.realm ? { realm: args.realm } : {} 7 | const coreFilter = args.core ? { core: args.core } : {} 8 | return await tags.find({ ...realmFilter, ...coreFilter }).toArray() 9 | }, 10 | 11 | updateTagMutationResolver: (parent, args) => updateTag(args.tag), 12 | deleteTagMutationResolver: (parent, args) => deleteTag(args.id) 13 | } 14 | 15 | export { tagResolvers } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/director.js" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /packages/core/src/network/graphql/coreTypes/realmInputType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLString 4 | } from 'graphql' 5 | 6 | const realmInputType = new GraphQLInputObjectType({ 7 | name: 'realmInputType', 8 | fields: { 9 | coreID: { 10 | type: GraphQLString 11 | }, 12 | id: { 13 | type: GraphQLString 14 | }, 15 | label: { 16 | type: GraphQLString 17 | }, 18 | description: { 19 | type: GraphQLString 20 | }, 21 | notes: { 22 | type: GraphQLString 23 | } 24 | } 25 | }) 26 | 27 | export default realmInputType 28 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/providerTypes/providerFunctionType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLList 5 | } from 'graphql' 6 | 7 | import parameterType from '../parameterTypes/parameterType' 8 | 9 | const providerFunctionType = new GraphQLObjectType({ 10 | name: 'providerFunction', 11 | fields: { 12 | id: { 13 | type: GraphQLString 14 | }, 15 | label: { 16 | type: GraphQLString 17 | }, 18 | parameters: { 19 | type: new GraphQLList(parameterType) 20 | } 21 | } 22 | }) 23 | 24 | export default providerFunctionType 25 | -------------------------------------------------------------------------------- /packages/ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 as build 2 | # Yarn workspaces are broken 3 | RUN apt-get update && apt-get install udev libusb-1.0-0-dev cmake build-essential -y 4 | RUN git clone https://phabricator.boreal.systems/source/Director.git /borealsystems/director --recursive 5 | WORKDIR /borealsystems/director 6 | RUN yarn workspace ui install 7 | RUN yarn workspace ui run build 8 | 9 | FROM nginx:1.15 10 | ADD ./nginx.conf /etc/nginx/conf.d/default.conf 11 | WORKDIR /borealsystems/director/ui 12 | ADD ./src/public/* ./public/ 13 | COPY --from=build /borealsystems/director/packages/ui/dist/* ./public/dist/ 14 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/coreTypes/coreType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLBoolean 5 | } from 'graphql' 6 | 7 | const coreConfigType = new GraphQLObjectType({ 8 | name: 'CoreConfigType', 9 | fields: { 10 | id: { 11 | type: GraphQLString 12 | }, 13 | label: { 14 | type: GraphQLString 15 | }, 16 | helpdeskVisable: { 17 | type: GraphQLBoolean 18 | }, 19 | helpdeskURI: { 20 | type: GraphQLString 21 | }, 22 | timezone: { 23 | type: GraphQLString 24 | } 25 | } 26 | }) 27 | 28 | export default coreConfigType 29 | -------------------------------------------------------------------------------- /packages/ui/src/View/components/ModalStateManager.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ModalStateManager = ({ LauncherContent, ModalContent }) => { 5 | const [open, setOpen] = useState(false) 6 | return ( 7 | <> 8 | {LauncherContent && } 9 | {ModalContent && } 10 | 11 | ) 12 | } 13 | 14 | ModalStateManager.propTypes = { 15 | LauncherContent: PropTypes.any, 16 | ModalContent: PropTypes.any 17 | } 18 | 19 | export default ModalStateManager 20 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/tagTypes/tagType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString 4 | } from 'graphql' 5 | 6 | import globalColourType from '../coreTypes/globalColourType' 7 | 8 | const tagType = new GraphQLObjectType({ 9 | name: 'tagType', 10 | fields: { 11 | core: { 12 | type: GraphQLString 13 | }, 14 | realm: { 15 | type: GraphQLString 16 | }, 17 | id: { 18 | type: GraphQLString 19 | }, 20 | label: { 21 | type: GraphQLString 22 | }, 23 | colour: { 24 | type: globalColourType 25 | } 26 | } 27 | }) 28 | 29 | export default tagType -------------------------------------------------------------------------------- /packages/core/src/network/graphql/coreTypes/coreInputType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLString, 4 | GraphQLBoolean 5 | } from 'graphql' 6 | 7 | const coreInputType = new GraphQLInputObjectType({ 8 | name: 'coreInputType', 9 | fields: { 10 | id: { 11 | type: GraphQLString 12 | }, 13 | label: { 14 | type: GraphQLString 15 | }, 16 | helpdeskVisable: { 17 | type: GraphQLBoolean 18 | }, 19 | helpdeskURI: { 20 | type: GraphQLString 21 | }, 22 | timezone: { 23 | type: GraphQLString 24 | } 25 | } 26 | }) 27 | 28 | export default coreInputType 29 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/coreTypes/realmType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString 4 | } from 'graphql' 5 | 6 | const realmType = new GraphQLObjectType({ 7 | name: 'realmType', 8 | fields: { 9 | coreID: { 10 | type: GraphQLString 11 | }, 12 | coreLabel: { 13 | type: GraphQLString 14 | }, 15 | id: { 16 | type: GraphQLString 17 | }, 18 | label: { 19 | type: GraphQLString 20 | }, 21 | description: { 22 | type: GraphQLString 23 | }, 24 | notes: { 25 | type: GraphQLString 26 | } 27 | } 28 | }) 29 | 30 | export default realmType 31 | -------------------------------------------------------------------------------- /packages/ui/src/View/Tags/queries.js: -------------------------------------------------------------------------------- 1 | const tagsQueryGQL = `query tags($realm: String, $core: String ) { 2 | tags(core: $core, realm: $realm) { 3 | id 4 | label 5 | core 6 | realm 7 | colour { 8 | id 9 | label 10 | } 11 | } 12 | globalColours { 13 | id 14 | label 15 | } 16 | }` 17 | 18 | const updateTagMutationGQL = `mutation updateTag($tag: tagInputType) { 19 | updateTag(tag:$tag) { 20 | id 21 | } 22 | }` 23 | 24 | const deleteTagMutationGQL = `mutation deleteTag($id: String) { 25 | deleteTag(id: $id) 26 | }` 27 | 28 | export { tagsQueryGQL, updateTagMutationGQL, deleteTagMutationGQL } 29 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/controllerTypes/controllerLayoutType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLInt 5 | } from 'graphql' 6 | 7 | const controllerLayoutType = new GraphQLObjectType({ 8 | name: 'controllerLayoutType', 9 | description: 'Controller Layout Type for both hardware and virtual controllers', 10 | fields: { 11 | id: { 12 | type: GraphQLString 13 | }, 14 | label: { 15 | type: GraphQLString 16 | }, 17 | rows: { 18 | type: GraphQLInt 19 | }, 20 | columns: { 21 | type: GraphQLInt 22 | } 23 | } 24 | }) 25 | 26 | export default controllerLayoutType 27 | -------------------------------------------------------------------------------- /packages/link/copy-builds.sh: -------------------------------------------------------------------------------- 1 | mkdir dist/macos 2 | mkdir dist/linux 3 | mkdir dist/windows 4 | mv dist/link-macos dist/macos/ 5 | mv dist/link-linux dist/linux/ 6 | mv dist/link-win.exe dist/windows/ 7 | mv dist/xdg-open dist/linux/ 8 | cp dist/HID.node dist/linux/ 9 | cp dist/HID.node dist/macos/ 10 | cp dist/HID.node dist/windows/ 11 | cp dist/index.node dist/linux/ 12 | cp dist/index.node dist/macos/ 13 | cp dist/index.node dist/windows/ 14 | cp dist/fsevents.node dist/linux/ 15 | cp dist/fsevents.node dist/macos/ 16 | cp dist/fsevents.node dist/windows/ 17 | zip -r dist/macos.zip dist/macos 18 | zip -r dist/linux.zip dist/linux 19 | zip -r dist/windows.zip dist/windows -------------------------------------------------------------------------------- /packages/core/readme.md: -------------------------------------------------------------------------------- 1 | # Boreal Systems Director Core 2 | 3 | This package is the Core of Boreal Director, the code here consists of all main elements required to spin up a Director install. For configuration you will also need Boreal Director UI (or roll your own with the GQL API, Documentation will exist at some point before the heat death of the universe.) You will probably also want either a hardware controller or Boreal Director Shotbox to actually execute stacks. 4 | 5 | # Developement 6 | (From Director root) 7 | ``` 8 | yarn workspace core 9 | yarn workspace core run dev 10 | ``` 11 | 12 | # Deployment 13 | Don't. This is not production ready in any way whatsoever. -------------------------------------------------------------------------------- /packages/core/src/network/graphql/tagTypes/tagInputType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLString 4 | } from 'graphql' 5 | 6 | import globalColourInputType from '../coreTypes/globalColourInputType' 7 | 8 | const tagInputType = new GraphQLInputObjectType({ 9 | name: 'tagInputType', 10 | fields: { 11 | core: { 12 | type: GraphQLString 13 | }, 14 | realm: { 15 | type: GraphQLString 16 | }, 17 | id: { 18 | type: GraphQLString 19 | }, 20 | label: { 21 | type: GraphQLString 22 | }, 23 | colour: { 24 | type: globalColourInputType 25 | } 26 | } 27 | }) 28 | 29 | export default tagInputType -------------------------------------------------------------------------------- /packages/core/src/panels/deletePanel.js: -------------------------------------------------------------------------------- 1 | import { panels } from '../db' 2 | import { panelWaterfall } from '../utils/waterfall' 3 | import log from '../utils/log' 4 | 5 | const deletePanel = (_id) => { 6 | return new Promise((resolve, reject) => { 7 | panels.findOne({ id: _id }) 8 | .then(panel => { 9 | panels.deleteOne({ id: _id }) 10 | return panel 11 | }) 12 | .then(panel => { 13 | log('info', 'core/panels', `Deleted panel ${panel.id} (${panel.label})`) 14 | panelWaterfall(panel, true) 15 | resolve(status.OK) 16 | }) 17 | .catch(e => reject(e)) 18 | }) 19 | } 20 | 21 | export default deletePanel 22 | -------------------------------------------------------------------------------- /packages/core/src/panels/panelResolvers.js: -------------------------------------------------------------------------------- 1 | import { panels } from '../db' 2 | import { getPanel, updatePanel, deletePanel } from '.' 3 | 4 | const panelResolvers = { 5 | panelsQuery: async (parent, args) => { 6 | const realmFilter = args.realm ? { realm: args.realm } : {} 7 | const coreFilter = args.core ? { core: args.core } : {} 8 | return await panels.find({ ...realmFilter, ...coreFilter }).toArray() 9 | }, 10 | 11 | panelQuery: (parent, args) => getPanel(args.id), 12 | panelMutationResolver: (parent, args) => updatePanel(args.panel), 13 | deletePanelMutationResolver: (parent, args) => deletePanel(args.id) 14 | } 15 | 16 | export { panelResolvers } 17 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/bridgeTypes/bridgeUpdateInputType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLString, 4 | GraphQLList 5 | } from 'graphql' 6 | 7 | import controllerInputType from '../controllerTypes/controllerInputType' 8 | 9 | const bridgeUpdateInputType = new GraphQLInputObjectType({ 10 | name: 'bridgeUpdateInputType', 11 | description: 'Input type for registering a bridge', 12 | fields: { 13 | type: { 14 | type: GraphQLString 15 | }, 16 | version: { 17 | type: GraphQLString 18 | }, 19 | controllers: { 20 | type: new GraphQLList(controllerInputType) 21 | } 22 | } 23 | }) 24 | 25 | export default bridgeUpdateInputType 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'plugin:react/recommended', 9 | 'standard' 10 | ], 11 | parser: "babel-eslint", 12 | globals: { 13 | Atomics: 'readonly', 14 | SharedArrayBuffer: 'readonly' 15 | }, 16 | parserOptions: { 17 | ecmaFeatures: { 18 | jsx: true 19 | }, 20 | ecmaVersion: 2018, 21 | sourceType: 'module' 22 | }, 23 | plugins: [ 24 | 'react', 25 | "react-hooks" 26 | ], 27 | rules: { 28 | "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks 29 | "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/network/apollo.js: -------------------------------------------------------------------------------- 1 | import log from '../utils/log' 2 | import { schema } from './graphql/schema' 3 | 4 | const { ApolloServer } = require('apollo-server') 5 | 6 | let port 7 | 8 | const initApollo = () => { 9 | port = process.env.NODE_ENV === 'development' ? 3001 : 3000 10 | 11 | const apollo = new ApolloServer({ 12 | schema: schema, 13 | context: ({ req }) => ({ req: req }) 14 | }) 15 | 16 | apollo.listen({ port: port }).then(({ url, subscriptionsUrl }) => { 17 | log('info', 'core/network/apollo', `Apollo 🚀 Server ready at ${url}`) 18 | log('info', 'core/network/apollo', `🚀 Subscriptions ready at ${subscriptionsUrl}`) 19 | }) 20 | } 21 | 22 | export { initApollo, port } 23 | -------------------------------------------------------------------------------- /packages/ui/src/View/components/NavGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | useRouteMatch 4 | } from 'react-router-dom' 5 | import { SideNavMenu } from 'carbon-components-react/lib/components/UIShell' 6 | 7 | function NavGroup ({ icon, label, grouppath, children }) { 8 | const match = useRouteMatch({ path: grouppath, exact: true }) 9 | if (match) { 10 | return ( 11 | 12 | {children} 13 | 14 | ) 15 | } else { 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | } 23 | 24 | export default NavGroup 25 | -------------------------------------------------------------------------------- /packages/ui/src/View/hooks/useWindowDimensions.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | function getWindowDimensions() { 4 | const { innerWidth: width, innerHeight: height } = window; 5 | return { 6 | width, 7 | height 8 | }; 9 | } 10 | 11 | export default function useWindowDimensions() { 12 | const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions()); 13 | 14 | useEffect(() => { 15 | function handleResize() { 16 | setWindowDimensions(getWindowDimensions()); 17 | } 18 | 19 | window.addEventListener('resize', handleResize); 20 | return () => window.removeEventListener('resize', handleResize); 21 | }, []); 22 | 23 | return windowDimensions; 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: borealsystems 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /packages/link/src/link.js: -------------------------------------------------------------------------------- 1 | import './tray' 2 | import 'isomorphic-unfetch' 3 | import { registerStreamdecks, updateStreamdecks } from './streamdeck' 4 | import { initExpress } from './network/express' 5 | import { initGQLClient } from './network/graphql' 6 | import { config } from './utils/config' 7 | 8 | config.init() 9 | .then(() => { 10 | initExpress() 11 | }) 12 | .then(() => { 13 | registerStreamdecks() 14 | }) 15 | .then(() => config.get('connection')) 16 | .then(connection => { 17 | if (connection && connection.host) { 18 | updateStreamdecks({ type: 'offline' }) 19 | initGQLClient(connection.host, connection.https) 20 | } else { 21 | updateStreamdecks({ type: 'unconfigured' }) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /packages/ui/src/View/Realms/realmQueries.js: -------------------------------------------------------------------------------- 1 | const createRealmMutationGQL = ` 2 | mutation createRealm($realm: realmInputType) { 3 | createRealm(realm: $realm) 4 | }` 5 | 6 | const deleteRealmMutationGQL = ` 7 | mutation deleteRealm($realm: realmInputType) { 8 | deleteRealm(realm: $realm) 9 | }` 10 | 11 | const updateRealmMutationGQL = ` 12 | mutation updateRealm($realm: realmInputType) { 13 | updateRealm(realm: $realm) 14 | }` 15 | 16 | const realmsQueryGQL = `{ 17 | realms { 18 | id 19 | label 20 | description 21 | notes 22 | coreID 23 | coreLabel 24 | } 25 | thisCore { 26 | id 27 | label 28 | } 29 | }` 30 | 31 | export { createRealmMutationGQL, deleteRealmMutationGQL, updateRealmMutationGQL, realmsQueryGQL } 32 | -------------------------------------------------------------------------------- /packages/core/src/controllers/publishControllerUpdate.js: -------------------------------------------------------------------------------- 1 | import { controllers } from '../db'; 2 | import { pubsub } from '../network/graphql/schema'; 3 | import { getPanel } from '../panels' 4 | 5 | const publishControllerUpdate = id => new Promise((resolve, reject) => { 6 | let controllerData 7 | let panelData 8 | let update 9 | controllers.findOne({ id: id }) 10 | .then(controller => controllerData = controller) 11 | .then(() => (getPanel(controllerData.panel.id))) 12 | .then(panel => panelData = panel) 13 | .then(() => update = { controller: { ...controllerData, panel: { ...panelData} }}) 14 | .then(() => pubsub.publish('CONTROLLER_UPDATE', update)) 15 | .catch((e) => reject(e)) 16 | .finally(() => resolve(update)) 17 | }) 18 | 19 | export default publishControllerUpdate 20 | -------------------------------------------------------------------------------- /packages/ui/src/View/Devices/DeviceWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useQuery } from 'urql' 3 | import { Loading } from 'carbon-components-react' 4 | import Device from './Device.jsx' 5 | 6 | import GraphQLError from '../components/GraphQLError.jsx' 7 | 8 | import { existingDeviceQuery, newDeviceQuery } from './queries' 9 | 10 | const DeviceWrapper = ({ match: { params: { id } } }) => { 11 | const [result] = useQuery({ 12 | query: id === 'new' ? newDeviceQuery : existingDeviceQuery, 13 | variables: id === 'new' ? null : { id: id } 14 | }) 15 | 16 | if (result.error) return 17 | if (result.fetching) { return } 18 | if (result.data) { return } 19 | } 20 | 21 | export default DeviceWrapper 22 | -------------------------------------------------------------------------------- /packages/core/src/bridges/index.js: -------------------------------------------------------------------------------- 1 | // import db from '../db' 2 | import log from '../utils/log' 3 | import { registerController } from "../controllers/registerController" 4 | import { uniqBy } from 'lodash' 5 | 6 | let bridges = [] 7 | 8 | const initBridges = () => {} 9 | 10 | const registerBridge = (_bridge) => { 11 | if (_bridge.controllers) { 12 | _bridge.controllers.map((controller) => { 13 | registerController(controller) 14 | }) 15 | } 16 | bridges.push(_bridge) 17 | bridges = uniqBy(bridges, (bridge) => { return bridge.address }) 18 | log('info', 'core/bridges', `Updated Bridge ${_bridge.type} with ${_bridge.controllers.length} controllers`) 19 | return `Updated Bridge ${_bridge.type} with ${_bridge.controllers.length} controllers` 20 | } 21 | 22 | export { initBridges, registerBridge, bridges } 23 | -------------------------------------------------------------------------------- /packages/ui/src/View/components/SideNavLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | useRouteMatch, 4 | useHistory 5 | } from 'react-router-dom' 6 | import { SideNavLink } from 'carbon-components-react/lib/components/UIShell' 7 | 8 | function CustomSideNavLink ({ label, to, renderIcon }) { 9 | const match = useRouteMatch({ path: to, exact: true }) 10 | const history = useHistory() 11 | if (match) { 12 | return ( 13 | { history.push(to) }}> 14 | {label} 15 | 16 | ) 17 | } else { 18 | return ( 19 | { history.push(to) }}> 20 | {label} 21 | 22 | ) 23 | } 24 | } 25 | 26 | export default CustomSideNavLink 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Want something added? 4 | title: '' 5 | labels: Feature Request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is this a request for device support?** 11 | If so, please see https://github.com/borealsystems/DefinitionLibrary and make a request there :) 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /packages/ui/src/View/components/ReactError.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Grid, Row, Column, ToastNotification } from 'carbon-components-react' 3 | 4 | const ReactError = () => { 5 | return ( 6 | 7 | 8 | 9 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default ReactError 30 | -------------------------------------------------------------------------------- /packages/core/src/network/rosstalk.js: -------------------------------------------------------------------------------- 1 | import log from '../utils/log' 2 | import { executeStack } from '../stacks' 3 | const Net = require('net'); 4 | const server = new Net.Server(); 5 | 6 | const port = 7788 7 | 8 | const initRossTalk = () => new Promise(resolve => { 9 | server.listen(port, () => { 10 | log('info', 'core/network/rosstalk', `🚀 RossTalk Server ready at tcp://0.0.0.0:${port}`) 11 | resolve() 12 | }) 13 | 14 | server.on('error', err => { 15 | log('error', 'core/network/rosstalk', err) 16 | }) 17 | 18 | server.on('connection', (socket) => { 19 | socket.on('data', (chunk) => { 20 | console.log(chunk.toString()) 21 | const match = chunk.toString().match(/(GPI [A-z,0-9]{9})/g) 22 | if (match?.[0]) { 23 | executeStack(match[0].slice(4), 'RossTalk') 24 | } 25 | }) 26 | }) 27 | }) 28 | 29 | export { initRossTalk } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Got a problem? 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Is this with the UI, the Core, or a Device Definition?** 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Core Info** 29 | - Version/Build 30 | - OS 31 | - OS Version/Build 32 | 33 | **UI Info** 34 | - Browser 35 | - Browser Version/Build 36 | - OS 37 | - OS Version/Build 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /packages/ui/src/View/Executer/queries.js: -------------------------------------------------------------------------------- 1 | const devicesQueryGQL = `query devices($realm: String, $core: String) { 2 | devices(realm: $realm, core: $core) { 3 | id 4 | label 5 | location 6 | description 7 | provider { 8 | id 9 | label 10 | } 11 | status 12 | } 13 | }` 14 | 15 | const deviceFunctionQueryGQL = `query deviceFunctions($id: String) { 16 | deviceFunctions(id: $id) { 17 | id 18 | label 19 | parameters { 20 | id 21 | label 22 | inputType 23 | regex 24 | items 25 | placeholder 26 | tooltip 27 | required 28 | invalidText 29 | min 30 | max 31 | } 32 | } 33 | }` 34 | 35 | const executeActionMutationGQL = ` 36 | mutation executeAction($action: stackActionInputType) { 37 | executeAction(action: $action) 38 | }` 39 | 40 | export { devicesQueryGQL, deviceFunctionQueryGQL, executeActionMutationGQL } 41 | -------------------------------------------------------------------------------- /docker-compose.canary.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | database: 4 | image: mongo 5 | environment: 6 | - MONGO_INITDB_DATABASE=DirectorCore 7 | - MONGO_INITDB_ROOT_USERNAME=Director 8 | - MONGO_INITDB_ROOT_PASSWORD=S3CuR3P4ssw0rd 9 | volumes: 10 | - /var/BorealSystems/Director/database:/data/db 11 | core: 12 | build: ./packages/core 13 | environment: 14 | - DIRECTOR_CORE_CONFIG_LABEL=Director 15 | - DIRECTOR_CORE_DB_HOST=database 16 | - DIRECTOR_CORE_DB_DATABASE=DirectorCore 17 | - DIRECTOR_CORE_DB_USERNAME=Director 18 | - DIRECTOR_CORE_DB_PASSWORD=S3CuR3P4ssw0rd 19 | - TZ=${TZ} 20 | volumes: 21 | - '/etc/localtime:/etc/localtime:ro' 22 | - type: bind 23 | source: /var/BorealSystems/Director/logs.txt 24 | target: /borealsystems/director/core/logs.txt 25 | ui: 26 | build: ./packages/ui 27 | ports: 28 | - 3000:80 29 | -------------------------------------------------------------------------------- /packages/ui/src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | Director 13 | 18 | 19 | 20 | 21 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/ui/src/View/Controllers/ControllerWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useQuery } from 'urql' 3 | import { Loading } from 'carbon-components-react' 4 | import Controller from './Controller.jsx' 5 | 6 | import GraphQLError from '../components/GraphQLError.jsx' 7 | 8 | import { controllerLayoutsQueryGQL, existingControllerQueryGQL } from './queries' 9 | 10 | const ControllerWrapper = ({ match: { params: { id } } }) => { 11 | const [result] = useQuery({ 12 | query: id === 'new' ? controllerLayoutsQueryGQL : existingControllerQueryGQL, 13 | variables: id === 'new' ? null : { id: id } 14 | }) 15 | 16 | if (result.error) return 17 | if (result.fetching) { return } 18 | if (result.data) { return } 19 | } 20 | 21 | export default ControllerWrapper 22 | -------------------------------------------------------------------------------- /packages/link/src/network/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | DirectorLink 13 | 18 | 19 | 20 | 21 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/ui/src/View/components/Clock.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { TooltipDefinition } from 'carbon-components-react' 3 | 4 | // eslint-disable-next-line react/prop-types 5 | function Clock ({ tz }) { 6 | const [time, setTime] = useState(new Date()) 7 | 8 | useEffect(() => { 9 | const loop = setTimeout(() => { 10 | setTime(new Date()) 11 | }, 100) 12 | 13 | const cleanup = () => { 14 | clearTimeout(loop) 15 | } 16 | return cleanup 17 | }) 18 | 19 | if (tz) { 20 | return ( 21 | 27 | {time.toLocaleTimeString('en-US', { timeZone: tz })} 28 | 29 | ) 30 | } else return null 31 | } 32 | 33 | export default Clock 34 | -------------------------------------------------------------------------------- /packages/ui/src/View/components/arrayUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Chunk an array 3 | * 4 | * @param {!number} chunkSize 5 | * @param {!Array.} array 6 | * @return {Array.} 7 | * 8 | * @example 9 | * arrayChunk(['Apple', 'Google', 'FaceBook'], 1) 10 | */ 11 | 12 | const arrayChunk = (array, chunkSize) => { 13 | var R = [] 14 | for (var i = 0; i < array.length; i += chunkSize) { 15 | R.push(array.slice(i, i + chunkSize)) 16 | } 17 | return R 18 | } 19 | 20 | /** 21 | * Pad an array to specified length 22 | * 23 | * @param {!number} length 24 | * @param {!Array.} array 25 | * @return {Array.} 26 | * 27 | * @example 28 | * arrayChunk(['Apple', 'Google', 'FaceBook'], 1) 29 | */ 30 | const arrayPad = (array, length) => { 31 | var padding = [] 32 | var padsToAdd = length - array.length 33 | for (var i = 0; i < padsToAdd; i++) { 34 | padding.push({ isPadding: true }) 35 | } 36 | return array.concat(padding) 37 | } 38 | 39 | export { arrayChunk, arrayPad } -------------------------------------------------------------------------------- /packages/link/src/streamdeck/utils/writePanel.js: -------------------------------------------------------------------------------- 1 | import { writeTextToButton } from '../graphics' 2 | import log from '../../utils/log' 3 | import buttonLUT from './buttonLUT' 4 | 5 | const writePanel = ({ panel, device }) => { 6 | panel.buttons.map((row, rowIndex) => { 7 | row.map((button, buttonIndex) => { 8 | if (button.stack !== null) { 9 | try { 10 | writeTextToButton({ text: button.stack.panelLabel || button.stack.label, device: device, buttonIndex: buttonLUT[device.config.manufacturer][device.config.model].forward[rowIndex][buttonIndex], background: button.stack.colour?.id }) 11 | } catch (e) { 12 | if (e.name === 'TypeError') { 13 | } else { 14 | log('warn', 'link/streamdeck/writePanel', `Error writing panel (${device.config.panel.id}, ${device.config.panel.label}) to device, panel is probably a different size: ${e}`) 15 | } 16 | } 17 | } 18 | }) 19 | }) 20 | } 21 | 22 | export default writePanel 23 | -------------------------------------------------------------------------------- /packages/ui/src/View/Panels/PanelWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { useQuery } from 'urql' 3 | import { Loading } from 'carbon-components-react' 4 | import { existingPanelGQL, newPanelGQL } from './queries' 5 | import globalContext from '../../globalContext' 6 | import Panel from './Panel.jsx' 7 | import GraphQLError from '../components/GraphQLError.jsx' 8 | 9 | const PanelWrapper = ({ match: { params: { id } } }) => { 10 | const { contextRealm } = useContext(globalContext) 11 | const [result] = useQuery({ 12 | query: id === 'new' ? newPanelGQL : existingPanelGQL, 13 | variables: id === 'new' ? { core: contextRealm.coreID, realm: contextRealm.id } : { id: id, core: contextRealm.coreID, realm: contextRealm.id } 14 | }) 15 | 16 | if (result.error) return 17 | if (result.fetching) { return } 18 | if (result.data) { return } 19 | } 20 | 21 | export default PanelWrapper 22 | -------------------------------------------------------------------------------- /packages/core/src/controllers/controllerResolvers.js: -------------------------------------------------------------------------------- 1 | import { updateController, deleteController, controllerLayouts } from '.' 2 | import { controllers } from '../db' 3 | 4 | const controllerResolvers = { 5 | controllerLayoutsQueryResolver: () => controllerLayouts, 6 | 7 | controllersQueryResolver: async (p, args) => { 8 | const realmFilter = args.realm ? { realm: args.realm } : {} 9 | const coreFilter = args.core ? { core: args.core } : {} 10 | return await controllers.find({ ...realmFilter, ...coreFilter }).toArray() 11 | }, 12 | 13 | controllerQueryResolver: (parent, args) => { 14 | return new Promise((resolve, reject) => { 15 | controllers.findOne({ id: args.id }) 16 | .then(controller => resolve(controller)) 17 | .catch(e => reject(e)) 18 | }) 19 | }, 20 | 21 | controllerMutationResolver: (parent, args) => updateController(args.controller), 22 | deleteControllerMutationResolver: (parent, args) => deleteController(args.id) 23 | } 24 | 25 | export { controllerResolvers } 26 | -------------------------------------------------------------------------------- /docker-compose.achilles.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | database: 4 | image: mongo 5 | environment: 6 | - MONGO_INITDB_DATABASE=DirectorCore 7 | - MONGO_INITDB_ROOT_USERNAME=Director 8 | - MONGO_INITDB_ROOT_PASSWORD=S3CuR3P4ssw0rd 9 | volumes: 10 | - /var/BorealSystems/Director/database:/data/db 11 | restart: always 12 | core: 13 | image: borealsystems/director:achilles-core 14 | environment: 15 | - DIRECTOR_CORE_CONFIG_LABEL=Director 16 | - DIRECTOR_CORE_DB_HOST=database 17 | - DIRECTOR_CORE_DB_DATABASE=DirectorCore 18 | - DIRECTOR_CORE_DB_USERNAME=Director 19 | - DIRECTOR_CORE_DB_PASSWORD=S3CuR3P4ssw0rd 20 | - TZ=${TZ} 21 | volumes: 22 | - '/etc/localtime:/etc/localtime:ro' 23 | - type: bind 24 | source: /var/BorealSystems/Director/logs.txt 25 | target: /borealsystems/director/core/logs.txt 26 | restart: always 27 | ui: 28 | image: borealsystems/director:achilles-ui 29 | restart: always 30 | ports: 31 | - 3000:80 -------------------------------------------------------------------------------- /packages/core/src/panels/updatePanel.js: -------------------------------------------------------------------------------- 1 | import { panels } from '../db' 2 | import { panelWaterfall } from '../utils/waterfall' 3 | import log from '../utils/log' 4 | import shortid from 'shortid' 5 | 6 | const updatePanel = (_panel) => { 7 | return new Promise((resolve, reject) => { 8 | const id = _panel.id ? _panel.id : shortid.generate() 9 | panels.updateOne( 10 | { id: id }, 11 | { 12 | $set: { 13 | core: _panel.core ? _panel.core : process.env.DIRECTOR_CORE_CONFIG_LABEL, 14 | realm: _panel.realm ? _panel.realm : 'root', 15 | id: id, 16 | ..._panel 17 | } 18 | }, 19 | { upsert: true } 20 | ) 21 | .then(() => { 22 | return panels.findOne({ id: id }) 23 | }) 24 | .then(panel => { 25 | log('info', 'core/panels', `${!panel.id ? 'Created' : 'Updated'} ${panel.id} (${panel.label})`) 26 | panelWaterfall(panel) 27 | resolve(panel) 28 | }) 29 | .catch(e => reject(e)) 30 | }) 31 | } 32 | 33 | export default updatePanel 34 | -------------------------------------------------------------------------------- /packages/core/src/controllers/registerController.js: -------------------------------------------------------------------------------- 1 | import { controllers } from '../db'; 2 | import log from '../utils/log'; 3 | import STATUS from '../utils/statusEnum'; 4 | 5 | const registerController = (_controller) => { 6 | return new Promise((resolve, reject) => { 7 | const id = `${_controller.manufacturer}-${_controller.model}-${_controller.serial}`; 8 | controllers.updateOne( 9 | {id: id}, 10 | { 11 | $set: { 12 | core: _controller.core? _controller.core:process.env.DIRECTOR_CORE_ID, 13 | realm: _controller.realm? _controller.realm:'ROOT', 14 | id: id, 15 | status: STATUS.OK, 16 | label: `${_controller.manufacturer}-${_controller.model}`, 17 | ..._controller 18 | } 19 | }, 20 | {upsert: true} 21 | ) 22 | .then(() => { 23 | log('info','core/controllers',`Registered ${id}`); 24 | resolve(controllers.findOne({id: id})); 25 | }) 26 | .catch(e => reject(e)); 27 | }); 28 | }; 29 | 30 | 31 | export default registerController 32 | -------------------------------------------------------------------------------- /packages/ui/src/View/Stacks/StackWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { useQuery } from 'urql' 3 | import { Loading } from 'carbon-components-react' 4 | import Stack from './Stack.jsx' 5 | import globalContext from '../../globalContext' 6 | 7 | import GraphQLError from '../components/GraphQLError.jsx' 8 | 9 | import { existingStackQueryGQL, newStackQueryGQL } from './queries' 10 | 11 | const StackWrapper = ({ match: { params: { id } } }) => { 12 | const { contextRealm } = useContext(globalContext) 13 | const [result] = useQuery({ 14 | query: id === 'new' ? newStackQueryGQL : existingStackQueryGQL, 15 | variables: id === 'new' ? { realm: contextRealm.id, core: contextRealm.coreID } : { id: id } 16 | }) 17 | 18 | if (result.error) return 19 | if (result.fetching) { return } 20 | if (result.data) { return } 21 | } 22 | 23 | export default StackWrapper 24 | -------------------------------------------------------------------------------- /packages/core/src/db.js: -------------------------------------------------------------------------------- 1 | import log from './utils/log' 2 | import { MongoClient } from 'mongodb' 3 | require('dotenv').config() 4 | 5 | const client = new MongoClient(`mongodb://${process.env.DIRECTOR_CORE_DB_USERNAME}:${process.env.DIRECTOR_CORE_DB_PASSWORD}@${process.env.DIRECTOR_CORE_DB_HOST}:27017?retryWrites=true&w=majority`, { useUnifiedTopology: true }) 6 | 7 | let cores 8 | let controllers 9 | let devices 10 | let panels 11 | let realms 12 | let stacks 13 | let tags 14 | 15 | const initDB = async () => { 16 | log('info', 'core/lib/db', 'Loading Database') 17 | await client.connect() 18 | const database = client.db(process.env.DIRECTOR_CORE_DB_DATABASE) 19 | 20 | cores = database.collection('cores') 21 | controllers = database.collection('controllers') 22 | devices = database.collection('devices') 23 | panels = database.collection('panels') 24 | realms = database.collection('realms') 25 | stacks = database.collection('stacks') 26 | tags = database.collection('tags') 27 | } 28 | 29 | export { cores, realms, devices, stacks, panels, controllers, tags, initDB } 30 | -------------------------------------------------------------------------------- /packages/core/src/utils/omitDeep.js: -------------------------------------------------------------------------------- 1 | const omit = require('lodash/omit') 2 | 3 | export default function omitDeep (input, props) { 4 | const omitDeepOnOwnProps = (obj) => { 5 | if (typeof input === 'undefined') { 6 | return input 7 | } 8 | 9 | if (!Array.isArray(obj) && !isObject(obj)) { 10 | return obj 11 | } 12 | 13 | if (Array.isArray(obj)) { 14 | return omitDeep(obj, props) 15 | } 16 | 17 | const o = {} 18 | for (const [key, value] of Object.entries(obj)) { 19 | o[key] = !isNil(value) ? omitDeep(value, props) : value 20 | } 21 | 22 | return omit(o, props) 23 | } 24 | 25 | if (arguments.length > 2) { 26 | props = Array.prototype.slice.call(arguments).slice(1) 27 | } 28 | 29 | if (Array.isArray(input)) { 30 | return input.map(omitDeepOnOwnProps) 31 | } 32 | 33 | return omitDeepOnOwnProps(input) 34 | } 35 | 36 | const isNil = (value) => { 37 | return value === null || value === undefined 38 | } 39 | 40 | const isObject = (obj) => { 41 | return Object.prototype.toString.call(obj) === '[object Object]' 42 | } 43 | -------------------------------------------------------------------------------- /packages/link/src/utils/omitDeep.js: -------------------------------------------------------------------------------- 1 | const omit = require('lodash/omit') 2 | 3 | export default function omitDeep (input, props) { 4 | const omitDeepOnOwnProps = (obj) => { 5 | if (typeof input === 'undefined') { 6 | return input 7 | } 8 | 9 | if (!Array.isArray(obj) && !isObject(obj)) { 10 | return obj 11 | } 12 | 13 | if (Array.isArray(obj)) { 14 | return omitDeep(obj, props) 15 | } 16 | 17 | const o = {} 18 | for (const [key, value] of Object.entries(obj)) { 19 | o[key] = !isNil(value) ? omitDeep(value, props) : value 20 | } 21 | 22 | return omit(o, props) 23 | } 24 | 25 | if (arguments.length > 2) { 26 | props = Array.prototype.slice.call(arguments).slice(1) 27 | } 28 | 29 | if (Array.isArray(input)) { 30 | return input.map(omitDeepOnOwnProps) 31 | } 32 | 33 | return omitDeepOnOwnProps(input) 34 | } 35 | 36 | const isNil = (value) => { 37 | return value === null || value === undefined 38 | } 39 | 40 | const isObject = (obj) => { 41 | return Object.prototype.toString.call(obj) === '[object Object]' 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/bridgeTypes/bridgeType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLList 5 | } from 'graphql' 6 | 7 | const bridgeType = new GraphQLObjectType({ 8 | name: 'bridgeRegisterInputType', 9 | description: 'Input type for registering a bridge', 10 | fields: { 11 | type: { 12 | type: GraphQLString 13 | }, 14 | address: { 15 | type: GraphQLString 16 | }, 17 | version: { 18 | type: GraphQLString 19 | }, 20 | controllers: { 21 | type: new GraphQLList( 22 | new GraphQLObjectType({ 23 | name: 'bridgeRegisterControllerInputType', 24 | description: 'Input type for registering a controller on a bridge', 25 | fields: { 26 | manufacturer: { 27 | type: GraphQLString 28 | }, 29 | model: { 30 | type: GraphQLString 31 | }, 32 | serial: { 33 | type: GraphQLString 34 | } 35 | } 36 | }) 37 | ) 38 | } 39 | } 40 | }) 41 | 42 | export default bridgeType 43 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/parameterTypes/parameterType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLList, 5 | GraphQLBoolean, 6 | GraphQLInt 7 | } from 'graphql' 8 | 9 | import GraphQLJSON from 'graphql-type-json' 10 | 11 | const parameterType = new GraphQLObjectType({ 12 | name: 'functionParameter', 13 | fields: { 14 | label: { 15 | type: GraphQLString 16 | }, 17 | id: { 18 | type: GraphQLString 19 | }, 20 | inputType: { 21 | type: GraphQLString 22 | }, 23 | regex: { 24 | type: GraphQLString 25 | }, 26 | items: { 27 | type: new GraphQLList( 28 | GraphQLJSON 29 | ) 30 | }, 31 | placeholder: { 32 | type: GraphQLString 33 | }, 34 | required: { 35 | type: GraphQLBoolean 36 | }, 37 | tooltip: { 38 | type: GraphQLString 39 | }, 40 | invalidText: { 41 | type: GraphQLString 42 | }, 43 | min: { 44 | type: GraphQLInt 45 | }, 46 | max: { 47 | type: GraphQLInt 48 | } 49 | } 50 | }) 51 | 52 | export default parameterType 53 | -------------------------------------------------------------------------------- /packages/ui/src/View/Shotbox/queries.js: -------------------------------------------------------------------------------- 1 | const controllerSubscriptionGQL = `subscription controller { 2 | controller { 3 | label 4 | manufacturer 5 | model 6 | serial 7 | status 8 | panel { 9 | id 10 | label 11 | description 12 | layout { 13 | id 14 | label 15 | rows 16 | columns 17 | } 18 | layoutType { 19 | id 20 | label 21 | } 22 | buttons { 23 | row 24 | column 25 | stack { 26 | id 27 | label 28 | panelLabel 29 | description 30 | colour { 31 | id 32 | } 33 | } 34 | } 35 | } 36 | id 37 | } 38 | }` 39 | 40 | const panelQueryGQL = `query panelData($id: String) { 41 | panel(id: $id) { 42 | id 43 | label 44 | buttons { 45 | row 46 | column 47 | stack { 48 | id 49 | label 50 | panelLabel 51 | description 52 | colour { 53 | id 54 | } 55 | } 56 | } 57 | } 58 | }` 59 | 60 | export { controllerSubscriptionGQL, panelQueryGQL } 61 | -------------------------------------------------------------------------------- /packages/ui/src/View/Dashboard/Status.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useQuery } from 'urql' 3 | 4 | import { InlineLoading, ToastNotification } from 'carbon-components-react' 5 | 6 | import GraphQLError from '../components/GraphQLError.jsx' 7 | 8 | const Status = () => { 9 | const [result] = useQuery({ 10 | query: '{ status }', 11 | pollInterval: 10000 12 | }) 13 | 14 | if (result.error) return 15 | if (result.fetching) return 16 | if (result.data) { 17 | return ( 18 | <> 19 |
{result.data.status[1]}
20 |
21 | {result.data.status[2]} 22 | 23 | // 35 | ) 36 | } 37 | } 38 | 39 | export default Status 40 | -------------------------------------------------------------------------------- /packages/core/src/providers/protocolProviders/ProtocolProviderTCP.js: -------------------------------------------------------------------------------- 1 | import ConnectionProviderTCP from '../connectionProviders/ConnectionProviderTCP' 2 | 3 | class ProtocolProviderTCP extends ConnectionProviderTCP { 4 | static providerRegistration = { 5 | id: 'ProtocolProviderTCP', 6 | label: 'Protocol: TCP', 7 | manufacturer: 'Generic', 8 | protocol: 'TCP', 9 | description: 'This generic TCP provider allows you to send custom commands to any device supporting TCP control.', 10 | category: 'Protocol', 11 | parameters: this.parameters, 12 | constructor: ProtocolProviderTCP 13 | } 14 | 15 | providerFunctions = [ 16 | { 17 | id: 'message', 18 | label: 'Send ASCII over TCP', 19 | parameters: [ 20 | { 21 | inputType: 'textInput', 22 | label: 'ASCII string', 23 | id: 'message' 24 | } 25 | ] 26 | } 27 | ] 28 | 29 | interface = (_action) => { 30 | switch (_action.providerFunction.id) { 31 | case 'message': 32 | this.connectionProviderInterface({ 33 | message: `${_action.parameters.message}\r` 34 | }) 35 | break 36 | } 37 | } 38 | } 39 | 40 | export default ProtocolProviderTCP 41 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | import { PubSub } from 'apollo-server' 2 | 3 | import { GraphQLObjectType } from 'graphql' 4 | 5 | // Types 6 | import controllerType from './controllerTypes/controllerType' 7 | import logType from './coreTypes/logType' 8 | 9 | const pubsub = new PubSub() 10 | 11 | const LOG_ADDED = 'LOG_ADDED' 12 | const BRIDGE_UPDATES = 'BRIDGE_UPDATES' 13 | const CONTROLLER_UPDATE = 'CONTROLLER_UPDATE' 14 | 15 | const subscriptions = new GraphQLObjectType({ 16 | name: 'Subscriptions', 17 | fields: { 18 | subscribeToLogs: { 19 | name: 'Subscribe to Logs', 20 | description: 'Returns the logs', 21 | type: logType, 22 | subscribe: () => pubsub.asyncIterator([LOG_ADDED]) 23 | }, 24 | 25 | bridgeUpdates: { 26 | name: 'Bridge Updates', 27 | description: 'Obselete', 28 | type: logType, 29 | subscribe: () => pubsub.asyncIterator([BRIDGE_UPDATES]) 30 | }, 31 | 32 | controller: { 33 | name: 'Controller Updates', 34 | description: 'Subscription for all controller updates', 35 | type: controllerType, 36 | subscribe: () => pubsub.asyncIterator([CONTROLLER_UPDATE]) 37 | } 38 | } 39 | }) 40 | 41 | export { subscriptions, pubsub } 42 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/deviceTypes/deviceUpdateInputType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLString, 3 | GraphQLList, 4 | GraphQLInputObjectType 5 | } from 'graphql' 6 | import { GraphQLJSONObject } from 'graphql-type-json' 7 | 8 | const deviceUpdateInputType = new GraphQLInputObjectType({ 9 | name: 'deviceUpdate', 10 | fields: { 11 | id: { 12 | type: GraphQLString 13 | }, 14 | label: { 15 | type: GraphQLString 16 | }, 17 | location: { 18 | type: GraphQLString 19 | }, 20 | description: { 21 | type: GraphQLString 22 | }, 23 | status: { 24 | type: GraphQLString 25 | }, 26 | realm: { 27 | type: GraphQLString 28 | }, 29 | core: { 30 | type: GraphQLString 31 | }, 32 | provider: { 33 | type: new GraphQLInputObjectType({ 34 | name: 'deviceProviderDetailInputType', 35 | fields: { 36 | id: { 37 | type: GraphQLString 38 | }, 39 | label: { 40 | type: GraphQLString 41 | } 42 | } 43 | }) 44 | }, 45 | configuration: { 46 | type: new GraphQLList( 47 | GraphQLJSONObject 48 | ) 49 | } 50 | } 51 | }) 52 | 53 | export default deviceUpdateInputType 54 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/deviceTypes/deviceType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLList 5 | } from 'graphql' 6 | import GraphQLJSON from 'graphql-type-json' 7 | import providerType from '../providerTypes/providerType' 8 | 9 | const deviceType = new GraphQLObjectType({ 10 | name: 'Device', 11 | fields: { 12 | label: { 13 | type: GraphQLString 14 | }, 15 | description: { 16 | type: GraphQLString 17 | }, 18 | location: { 19 | type: GraphQLString 20 | }, 21 | id: { 22 | type: GraphQLString 23 | }, 24 | realm: { 25 | type: GraphQLString 26 | }, 27 | core: { 28 | type: GraphQLString 29 | }, 30 | provider: { 31 | type: providerType 32 | }, 33 | status: { 34 | type: GraphQLString 35 | }, 36 | configuration: { 37 | type: new GraphQLList( 38 | new GraphQLObjectType({ 39 | name: 'deviceConfigurationObject', 40 | fields: { 41 | id: { 42 | type: GraphQLString 43 | }, 44 | value: { 45 | type: GraphQLJSON 46 | } 47 | } 48 | }) 49 | ) 50 | } 51 | } 52 | }) 53 | 54 | export default deviceType 55 | -------------------------------------------------------------------------------- /packages/link/src/streamdeck/graphics/index.js: -------------------------------------------------------------------------------- 1 | import jpg from '@julusian/jpeg-turbo' 2 | const { Canvas } = require('skia-canvas') 3 | 4 | const getBuffer = canvas => { 5 | return jpg 6 | .decompressSync( 7 | canvas.toBuffer( 8 | 'jpg' 9 | ), 10 | { 11 | format: jpg.FORMAT_RGB 12 | } 13 | ) 14 | .data 15 | } 16 | 17 | const writeTextToButton = ({ 18 | text, 19 | device, 20 | buttonIndex, 21 | background, 22 | color 23 | }) => { 24 | const font = { 25 | original: 'bold 8pt Menlo', 26 | mini: 'bold 9pt Menlo', 27 | xl: 'bold 11pt Menlo' 28 | } 29 | 30 | const width = device.controller.ICON_SIZE 31 | const height = device.controller.ICON_SIZE 32 | const canvas = new Canvas(width, height) 33 | const ctx = canvas.getContext('2d') 34 | 35 | ctx.fillStyle = background ?? '#0043ce' 36 | ctx.fillRect(0, 0, width, height) 37 | ctx.fillStyle = color ?? 'white' 38 | ctx.font = font[device.model] 39 | ctx.textAlign = 'center' 40 | ctx.textBaseline = 'ideographic' 41 | ctx.textWrap = true 42 | ctx.fillText(text, width / 2, height / 2, width - 6) 43 | 44 | device.controller.fillImage(buttonIndex, getBuffer(canvas)) 45 | return true 46 | } 47 | 48 | export { writeTextToButton } 49 | -------------------------------------------------------------------------------- /packages/core/src/tags/index.js: -------------------------------------------------------------------------------- 1 | import log from '../utils/log' 2 | import status from '../utils/statusEnum' 3 | import { tags, stacks } from '../db' 4 | 5 | const updateTag = tag => new Promise((resolve, reject) => { 6 | tags.findOneAndUpdate( 7 | { id: tag.id }, 8 | { 9 | $set: { 10 | core: tag.core ? tag.core : process.env.DIRECTOR_CORE_CONFIG_LABEL, 11 | realm: tag.realm ? tag.realm : 'root', 12 | ...tag 13 | } 14 | }, 15 | { upsert: true }, 16 | (err, result) => { 17 | if (err) reject(err) 18 | log('info', 'core/tags', `Updated ${result.value.id} (${result.value.label})`) 19 | resolve(result.value) 20 | } 21 | ) 22 | }) 23 | 24 | const deleteTag = id => new Promise((resolve, reject) => { 25 | stacks.updateMany( 26 | { tags: { $elemMatch: { id: id }}}, 27 | { 28 | $pull: { 29 | tags: { 30 | id: id 31 | } 32 | } 33 | } 34 | ) 35 | .then(() => tags.findOneAndDelete({ id: id }, (err, result) => { 36 | if (err) reject(err) 37 | log('info', 'core/tags', `Deleted Tag ${result.value.id} ${result.value.label}`) 38 | })) 39 | .catch(err => reject(err)) 40 | .finally(() => resolve(status.OK)) 41 | }) 42 | 43 | export { updateTag, deleteTag } 44 | -------------------------------------------------------------------------------- /packages/link/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const port = process.env.PORT || 3010 4 | 5 | module.exports = { 6 | mode: 'development', 7 | 8 | entry: [ 9 | './src/network/ui/src/index.jsx' 10 | ], 11 | 12 | output: { 13 | path: path.resolve(__dirname, './src/network/ui/dist'), 14 | publicPath: '/dist/', 15 | filename: 'bundle.js' 16 | }, 17 | 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|jsx)$/, 22 | exclude: /(node_modules|bower_components)/, 23 | loader: ['react-hot-loader/webpack', 'babel-loader'] 24 | }, 25 | { 26 | test: /\.(png|jpe?g|gif|svg)$/i, 27 | loader: 'file-loader', 28 | options: { 29 | name: '[name].[ext]' 30 | } 31 | }, 32 | { 33 | test: /\.s[ac]ss$/i, 34 | use: [ 35 | 'style-loader', 36 | 'css-loader', 37 | 'sass-loader' 38 | ] 39 | } 40 | ] 41 | }, 42 | 43 | devServer: { 44 | host: '0.0.0.0', 45 | port: port, 46 | historyApiFallback: true, 47 | open: false, 48 | disableHostCheck: true, 49 | contentBase: path.join(__dirname, './src/network/ui/public/'), 50 | publicPath: `http://localhost:${port}/dist/`, 51 | hot: false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/src/controllers/updateController.js: -------------------------------------------------------------------------------- 1 | import {controllers} from '../db' 2 | import log from '../utils/log' 3 | import shortid from 'shortid' 4 | import STATUS from '../utils/statusEnum' 5 | import { publishControllerUpdate } from './publishControllerUpdate' 6 | 7 | const updateController = (_controller) => { 8 | const generatedid = shortid.generate() 9 | const id = _controller.id ? _controller.id : `virtual-${generatedid}` 10 | return new Promise((resolve, reject) => { 11 | controllers.updateOne( 12 | { id: id }, 13 | { 14 | $set: { 15 | core: _controller.core ? _controller.core : process.env.DIRECTOR_CORE_ID, 16 | realm: _controller.realm ? _controller.realm : 'ROOT', 17 | manufacturer: 'Boreal Systems', 18 | model: 'Virtual Controller', 19 | serial: generatedid, 20 | status: STATUS.OK, 21 | ..._controller 22 | } 23 | }, 24 | { upsert: true } 25 | ) 26 | .then(() => (publishControllerUpdate(id))) 27 | .then((controller) => { 28 | log('info', 'core/controllers', _controller.id ? `Updated ${id} (${controller.label}, ${controller.panel.label})` : `Created ${id}`) 29 | }) 30 | .catch(e => reject(e)) 31 | }) 32 | } 33 | 34 | export default updateController 35 | -------------------------------------------------------------------------------- /packages/link/src/streamdeck/utils/handleButtonPress.js: -------------------------------------------------------------------------------- 1 | import { director } from '../../network/graphql' 2 | import buttonLUT from './buttonLUT' 3 | import log from '../../utils/log' 4 | 5 | const executeStackMutationGQL = `mutation executeStack($id: String, $controller: String) { 6 | executeStack(id: $id, controller: $controller) 7 | }` 8 | 9 | const handleButtonPress = (device, index) => { 10 | // Totally readable, goes through LUT and translates the button ID for the specified device to the row/column IDs, and then returns the stack ID from the panel 11 | const stack = device.config.panel.buttons[buttonLUT[device.config.manufacturer][device.config.model].reverse[index].row]?.[buttonLUT[device.config.manufacturer][device.config.model].reverse[index].column]?.stack 12 | if (stack) { 13 | director.query(executeStackMutationGQL, { id: stack.id, controller: `${device.config.manufacturer}-${device.config.model}-${device.config.serial}` }) 14 | .toPromise() 15 | .then(result => { 16 | if (result.error) { 17 | log('info', 'link/streamdeck/handleButtonPress', result.error) 18 | } else { 19 | log('info', 'link/streamdeck/handleButtonPress', `Executed Stack ${stack.id} (${stack.label})`) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | export default handleButtonPress 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "packages/core/src/providers/deviceProviders/director-provider-resolume-arena"] 2 | path = packages/core/src/providers/deviceProviders/director-provider-resolume-arena 3 | url = https://github.com/borealsystems/director-provider-resolume-arena.git 4 | [submodule "packages/core/src/providers/deviceProviders/director-provider-bitfocus-companion"] 5 | path = packages/core/src/providers/deviceProviders/director-provider-bitfocus-companion 6 | url = https://github.com/borealsystems/director-provider-bitfocus-companion.git 7 | [submodule "packages/core/src/providers/deviceProviders/director-provider-harris-panacea"] 8 | path = packages/core/src/providers/deviceProviders/director-provider-harris-panacea 9 | url = https://github.com/borealsystems/director-provider-harris-panacea.git 10 | [submodule "packages/core/src/providers/deviceProviders/director-provider-ross-nk-ips"] 11 | path = packages/core/src/providers/deviceProviders/director-provider-ross-nk-ips 12 | url = https://github.com/borealsystems/director-provider-ross-nk-ips.git 13 | [submodule "packages/core/src/providers/deviceProviders/director-provider-ross-carbonite-black-plus"] 14 | path = packages/core/src/providers/deviceProviders/director-provider-ross-carbonite-black-plus 15 | url = https://github.com/borealsystems/director-provider-ross-carbonite-black-plus.git 16 | -------------------------------------------------------------------------------- /packages/link/src/utils/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import log from './log' 4 | 5 | const persistantConfig = path.join(process.env.APPDATA || (process.platform === 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + '/.config'), 'BorealSystems', 'DirectorLink', 'config.json') 6 | 7 | const config = { 8 | _config: {}, 9 | get: key => config._config[key], 10 | set: (key, value) => { 11 | config._config[key] = value 12 | fs.writeFile(persistantConfig, JSON.stringify(config._config), 'utf8', (err) => { 13 | if (err) log('error', 'link/utils/config', err) 14 | }) 15 | }, 16 | init: () => new Promise((resolve, reject) => { 17 | fs.mkdir(path.dirname(persistantConfig), { recursive: true }, err => { 18 | if (err) reject(err) 19 | fs.readFile(persistantConfig, 'utf8', (err, data) => { 20 | if (err) { 21 | if (err.code === 'ENOENT') { 22 | fs.writeFile(persistantConfig, JSON.stringify(config._config), 'utf8', (err) => { 23 | if (err) reject(err) 24 | }) 25 | resolve(config._config) 26 | } else reject(err) 27 | } else { 28 | config._config = JSON.parse(data || '{}') 29 | resolve(config._config) 30 | } 31 | }) 32 | }) 33 | }) 34 | } 35 | 36 | export { config } 37 | -------------------------------------------------------------------------------- /packages/ui/src/View/Dashboard/Notes.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { useQuery } from 'urql' 3 | import Markdown from 'react-markdown' 4 | import globalContext from '../../globalContext' 5 | 6 | import { InlineLoading } from 'carbon-components-react' 7 | 8 | import GraphQLError from '../components/GraphQLError.jsx' 9 | 10 | const Notes = () => { 11 | const { contextRealm } = useContext(globalContext) 12 | const [result] = useQuery({ 13 | query: `query realm($realm: String, $core: String) { 14 | realm(realm: $realm, core:$core) { 15 | notes 16 | } 17 | }`, 18 | pollInterval: 10000, 19 | variables: { realm: contextRealm.id, core: contextRealm.coreID } 20 | }) 21 | 22 | if (result.error) return 23 | if (result.fetching) { 24 | return ( 25 | <> 26 |
{contextRealm.coreLabel} {contextRealm.id === 'ROOT' ? 'root' : `/ ${contextRealm.label}` }

27 | 28 | 29 | ) 30 | } 31 | if (result.data) { 32 | return ( 33 | <> 34 |
{contextRealm.coreLabel} {contextRealm.id === 'ROOT' ? 'root' : `/ ${contextRealm.label}` }

35 | 39 | 40 | ) 41 | } 42 | } 43 | 44 | export default Notes 45 | -------------------------------------------------------------------------------- /packages/link/src/network/ui/src/index.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import React, { useState } from 'react' 3 | import { Provider, Client } from 'urql' 4 | import View from './View.jsx' 5 | import globalContext from './globalContext' 6 | 7 | const client = new Client({ 8 | url: '/graphql', 9 | requestPolicy: 'network-only', 10 | maskTypename: true 11 | }) 12 | 13 | const App = () => { 14 | const darkClass = 'dx--dark' 15 | const lightClass = 'dx--light' 16 | 17 | const [theme, _setTheme] = useState(localStorage.getItem('uiTheme') || darkClass) 18 | 19 | const toggleTheme = () => { 20 | switch (theme) { 21 | case darkClass : 22 | document.body.classList.remove(darkClass) 23 | document.body.classList.add(lightClass) 24 | localStorage.setItem('uiTheme', lightClass) 25 | _setTheme(lightClass) 26 | break 27 | case lightClass : 28 | document.body.classList.remove(lightClass) 29 | document.body.classList.add(darkClass) 30 | localStorage.setItem('uiTheme', darkClass) 31 | _setTheme(darkClass) 32 | break 33 | } 34 | } 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | ReactDOM.render(, document.getElementById('root')) 46 | -------------------------------------------------------------------------------- /packages/core/src/providers/protocolProviders/ProtocolProviderRossTalk.js: -------------------------------------------------------------------------------- 1 | import ConnectionProviderTCP from '../connectionProviders/ConnectionProviderTCP' 2 | 3 | class ProtocolProviderRossTalk extends ConnectionProviderTCP { 4 | static providerRegistration = { 5 | id: 'ProtocolProviderRossTalk', 6 | label: 'Protocol: RossTalk', 7 | manufacturer: 'Generic', 8 | protocol: 'RossTalk', 9 | description: 'This generic RossTalk provider allows you to send custom commands to any device supporting RossTalk control.', 10 | category: 'Protocol', 11 | parameters: this.parameters, 12 | defaults: [null, 7788], 13 | constructor: ProtocolProviderRossTalk 14 | } 15 | 16 | providerFunctions = [ 17 | { 18 | id: 'message', 19 | label: 'Send RossTalk Message', 20 | parameters: [ 21 | { 22 | inputType: 'textInput', 23 | id: 'message', 24 | label: 'Message', 25 | required: true, 26 | placeholder: 'Message to send', 27 | tooltip: 'RossTalk messages usually consist of a keyword followed by parameters' 28 | } 29 | ] 30 | } 31 | ] 32 | 33 | interface = (_action) => { 34 | switch (_action.providerFunction.id) { 35 | case 'message': 36 | this.connectionProviderInterface({ 37 | message: `${_action.parameters.message}\r` 38 | }) 39 | break 40 | } 41 | } 42 | } 43 | 44 | export default ProtocolProviderRossTalk 45 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/controllerTypes/controllerType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString 4 | } from 'graphql' 5 | 6 | import panelType from '../panelTypes/panelType' 7 | import controllerLayoutType from './controllerLayoutType' 8 | 9 | const controllerType = new GraphQLObjectType({ 10 | name: 'controllerType', 11 | description: 'Controller Type for both hardware and bridged controllers', 12 | fields: { 13 | manufacturer: { 14 | type: GraphQLString 15 | }, 16 | model: { 17 | type: GraphQLString 18 | }, 19 | serial: { 20 | type: GraphQLString 21 | }, 22 | label: { 23 | type: GraphQLString 24 | }, 25 | description: { 26 | type: GraphQLString 27 | }, 28 | panel: { 29 | type: panelType 30 | }, 31 | layout: { 32 | type: controllerLayoutType 33 | }, 34 | type: { 35 | type: new GraphQLObjectType({ 36 | name: 'controllerLayoutTypeType', 37 | fields: { 38 | id: { 39 | type: GraphQLString 40 | }, 41 | label: { 42 | type: GraphQLString 43 | } 44 | } 45 | }) 46 | }, 47 | id: { 48 | type: GraphQLString 49 | }, 50 | status: { 51 | type: GraphQLString 52 | }, 53 | core: { 54 | type: GraphQLString 55 | }, 56 | realm: { 57 | type: GraphQLString 58 | } 59 | } 60 | }) 61 | 62 | export default controllerType 63 | -------------------------------------------------------------------------------- /packages/link/src/network/express.js: -------------------------------------------------------------------------------- 1 | 2 | import { schema } from './schema' 3 | import express from 'express' 4 | import webpack from 'webpack' 5 | import middleware from 'webpack-dev-middleware' 6 | import path from 'path' 7 | import cors from 'cors' 8 | import log from '../utils/log' 9 | const { graphqlHTTP } = require('express-graphql') 10 | const webpackConfiguration = require('../../webpack.config.js') 11 | 12 | const initExpress = () => { 13 | const app = express() 14 | const dev = process.env.NODE_ENV === 'development' 15 | 16 | app.use(cors()) 17 | 18 | // dev && app.use(middleware( 19 | // webpack(webpackConfiguration), 20 | // { publicPath: '/dist' } 21 | // )) 22 | 23 | app.use('/graphql', graphqlHTTP({ 24 | schema: schema, 25 | graphiql: dev 26 | })) 27 | 28 | app.get('/dist/bundle.js', (req, res) => { 29 | res.sendFile(path.join(__dirname, './ui/dist/bundle.js')) 30 | }) 31 | 32 | app.get('/dist/connected_world.svg', (req, res) => { 33 | res.sendFile(path.join(__dirname, './ui/dist/connected_world.svg')) 34 | }) 35 | 36 | app.get('/favicon.ico', (req, res) => { 37 | res.sendFile(path.join(__dirname, './ui/public/favicon.ico')) 38 | }) 39 | 40 | app.get('/*', (req, res) => { 41 | res.sendFile(path.join(__dirname, './ui/public/index.html')) 42 | }) 43 | 44 | const port = 3010 45 | 46 | app.listen(port, () => { 47 | log('info', 'network/express', `🚀 Configuration UI ready at http://localhost:${port}`) 48 | }) 49 | } 50 | 51 | export { initExpress } 52 | -------------------------------------------------------------------------------- /packages/link/src/network/ui/src/GraphQLError.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { ToastNotification } from 'carbon-components-react' 4 | 5 | const GraphQLError = (props) => { 6 | if (props.error.networkError) { 7 | return ( 8 | 23 | ) 24 | } else if (props.error.graphQLErrors.length > 0) { 25 | return ( 26 | 41 | ) 42 | } 43 | } 44 | 45 | GraphQLError.propTypes = { 46 | error: PropTypes.any 47 | } 48 | 49 | export default GraphQLError 50 | -------------------------------------------------------------------------------- /packages/ui/src/View/components/GraphQLError.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { ToastNotification } from 'carbon-components-react' 4 | 5 | const GraphQLError = (props) => { 6 | if (props.error.networkError) { 7 | return ( 8 | 23 | ) 24 | } else if (props.error.graphQLErrors.length > 0) { 25 | return ( 26 | 41 | ) 42 | } 43 | } 44 | 45 | GraphQLError.propTypes = { 46 | error: PropTypes.any 47 | } 48 | 49 | export default GraphQLError 50 | -------------------------------------------------------------------------------- /packages/ui/src/View/Shotbox/ShotboxPanelWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Loading } from 'carbon-components-react' 4 | import { useQuery } from 'urql' 5 | import ShotboxPanel from './ShotboxPanel.jsx' 6 | import GraphQLError from '../components/GraphQLError.jsx' 7 | import { panelQueryGQL } from './queries' 8 | 9 | const ShotboxPanelWrapper = ({ inline, match: { params: { id } }, controller }) => { 10 | const [panel, setPanel] = useState({}) 11 | const [result] = useQuery({ 12 | query: panelQueryGQL, 13 | variables: { id: id }, 14 | pause: controller?.panel?.buttons ?? !id 15 | }) 16 | 17 | if (result.error) { return } 18 | if (result.fetching) { return } 19 | if (result.data && !panel.id) { 20 | const buttons = [] 21 | result.data.panel.buttons.map(row => { buttons.push(Object.keys(row).map(function (key) { return row[key] })) }) 22 | setPanel({ ...result.data.panel, buttons: buttons }) 23 | return 24 | } 25 | if (controller?.panel?.buttons) { 26 | return 27 | } 28 | if (!controller?.panel?.buttons && panel.id) { 29 | return 30 | } 31 | } 32 | 33 | ShotboxPanelWrapper.propTypes = { 34 | match: PropTypes.object, 35 | inline: PropTypes.bool, 36 | controller: PropTypes.object 37 | } 38 | 39 | export default ShotboxPanelWrapper 40 | -------------------------------------------------------------------------------- /packages/link/src/streamdeck/queries.js: -------------------------------------------------------------------------------- 1 | const updateBridgeMutationGQL = ` 2 | mutation updateBridge($bridge: bridgeUpdateInputType) { 3 | updateBridge(bridge: $bridge) 4 | }` 5 | 6 | const panelGQL = ` 7 | query panel($id: String) { 8 | panel(id: $id) { 9 | id 10 | label 11 | buttons { 12 | row 13 | column 14 | stack { 15 | colour { 16 | id 17 | label 18 | } 19 | id 20 | label 21 | panelLabel 22 | description 23 | } 24 | } 25 | } 26 | }` 27 | 28 | const controllersQueryGQL = ` 29 | query controllers { 30 | controllers { 31 | manufacturer 32 | model 33 | serial 34 | status 35 | realm 36 | core 37 | panel { 38 | label 39 | id 40 | } 41 | id 42 | label 43 | } 44 | }` 45 | 46 | const controllerUpdateSubscriptionGQL = ` 47 | subscription controller { 48 | controller { 49 | label 50 | manufacturer 51 | model 52 | serial 53 | status 54 | panel { 55 | id 56 | label 57 | buttons { 58 | row 59 | column 60 | stack { 61 | colour { 62 | id 63 | label 64 | } 65 | id 66 | label 67 | panelLabel 68 | description 69 | } 70 | } 71 | } 72 | id 73 | } 74 | }` 75 | 76 | export { updateBridgeMutationGQL, panelGQL, controllersQueryGQL, controllerUpdateSubscriptionGQL } 77 | -------------------------------------------------------------------------------- /packages/ui/src/View/Controllers/queries.js: -------------------------------------------------------------------------------- 1 | const controllersQueryGQL = `query controllers($realm: String, $core: String) { 2 | controllers(realm: $realm, core: $core) { 3 | label 4 | manufacturer 5 | model 6 | serial 7 | status 8 | panel { 9 | id 10 | label 11 | } 12 | id 13 | } 14 | panels { 15 | id 16 | label 17 | } 18 | }` 19 | 20 | const deleteControllerMutationGQL = `mutation deleteController($id: String) { 21 | deleteController(id: $id) 22 | } 23 | ` 24 | 25 | const controllerLayoutsQueryGQL = `query controllerLayouts { 26 | controllerLayouts { 27 | id 28 | label 29 | rows 30 | columns 31 | } 32 | panels { 33 | id 34 | label 35 | } 36 | }` 37 | 38 | const existingControllerQueryGQL = `query controller($id: String) { 39 | controller(id: $id) { 40 | label 41 | manufacturer 42 | description 43 | model 44 | serial 45 | status 46 | type { 47 | id 48 | label 49 | } 50 | layout { 51 | id 52 | label 53 | rows 54 | columns 55 | } 56 | panel { 57 | id 58 | label 59 | } 60 | id 61 | } 62 | panels { 63 | id 64 | label 65 | } 66 | controllerLayouts { 67 | id 68 | label 69 | rows 70 | columns 71 | } 72 | } 73 | ` 74 | 75 | const controllerUpdateMutationGQL = ` 76 | mutation controller($controller: controllerInputType) { 77 | controller(controller: $controller) { 78 | id 79 | } 80 | } 81 | ` 82 | 83 | export { controllersQueryGQL, controllerUpdateMutationGQL, deleteControllerMutationGQL, controllerLayoutsQueryGQL, existingControllerQueryGQL } 84 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core", 3 | "version": "0.3.3", 4 | "description": "The core application for the Boreal Director orchestation suite", 5 | "main": "index.js", 6 | "repository": "https://github.com/borealsystems/Director", 7 | "author": "Oliver Herrmann", 8 | "license": "GPL-3.0", 9 | "scripts": { 10 | "build": "babel src -d dist", 11 | "prod": "node dist/main.js", 12 | "dev": "NODE_ENV=development nodemon --ignore src/ui --signal SIGHUP --exec babel-node src/main.js" 13 | }, 14 | "devDependencies": { 15 | "@babel/cli": "^7.8.4", 16 | "@babel/core": "^7.8.4", 17 | "@babel/node": "^7.8.4", 18 | "@babel/plugin-proposal-class-properties": "^7.12.1", 19 | "@babel/preset-env": "^7.8.4", 20 | "babel-eslint": "^10.1.0", 21 | "babel-loader": "^8.0.6", 22 | "eslint": "^6.8.0", 23 | "eslint-config-standard": "^14.1.0", 24 | "eslint-plugin-import": "^2.20.1", 25 | "eslint-plugin-node": "^11.0.0", 26 | "eslint-plugin-promise": "^4.2.1", 27 | "eslint-plugin-react": "^7.18.3", 28 | "eslint-plugin-standard": "^4.0.1", 29 | "express-http-proxy": "^1.6.2", 30 | "nodemon": "^2.0.2" 31 | }, 32 | "dependencies": { 33 | "@serialport/bindings": "^9.0.2", 34 | "apollo-server": "^2.16.0", 35 | "babel-preset-es2015": "^6.24.1", 36 | "chalk": "^3.0.0", 37 | "debug": "^4.1.1", 38 | "dotenv": "^8.2.0", 39 | "express-graphql": "^0.9.0", 40 | "graphql": "^14.6.0", 41 | "graphql-type-json": "^0.3.2", 42 | "lodash": "^4.17.15", 43 | "mongodb": "^3.6.1", 44 | "nicely-format": "^1.1.0", 45 | "osc": "^2.4.0", 46 | "shortid": "^2.2.15" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/devices/deviceResolvers.js: -------------------------------------------------------------------------------- 1 | import { devices } from '../db' 2 | import { updateDevice, deleteDevice, disableDevice, enableDevice, deviceInstance } from '.' 3 | import { providers } from '../providers' 4 | 5 | const deviceResolvers = { 6 | devicesQueryResolver: async (p, args) => { 7 | const realmFilter = args.realm ? { realm: args.realm } : {} 8 | const coreFilter = args.core ? { core: args.core } : {} 9 | const devicesArray = await devices.find({ ...realmFilter, ...coreFilter }).toArray() 10 | const coresArray = args.realm === 'ROOT' ? [] : await devices.find({ 'provider.id': 'ProtocolProviderBorealDirector' }).toArray() 11 | return [...devicesArray.map(device => ({ ...device, provider: providers.find(provider => provider.id === device.provider.id) })), ...coresArray] 12 | }, 13 | 14 | deviceQueryResolver: async (parent, args) => { 15 | const device = await devices.findOne({ id: args.id }) 16 | return { ...device, provider: providers.find(provider => provider.id === device.provider.id) } 17 | }, 18 | 19 | deviceFunctionsQueryResolver: (p, args) => { 20 | if (deviceInstance[args.id]) { 21 | if (typeof deviceInstance[args.id].providerFunctions === 'function') return deviceInstance[args.id].providerFunctions(args) 22 | else return deviceInstance[args.id].providerFunctions 23 | } else return [] 24 | }, 25 | 26 | deviceMutationResolver: (parent, args) => updateDevice(args.device), 27 | deleteDeviceMutationResolver: (parent, args) => deleteDevice(args.id), 28 | disableDeviceMutationResolver: (parent, args) => disableDevice(args.id), 29 | enableDeviceMutationResolver: (parent, args) => enableDevice(args.id) 30 | } 31 | 32 | export { deviceResolvers } -------------------------------------------------------------------------------- /packages/core/src/network/graphql/providerTypes/providerType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLList, 5 | GraphQLBoolean 6 | } from 'graphql' 7 | 8 | import { GraphQLJSONObject } from 'graphql-type-json' 9 | 10 | const providerType = new GraphQLObjectType({ 11 | name: 'Provider', 12 | fields: { 13 | id: { 14 | type: GraphQLString 15 | }, 16 | label: { 17 | type: GraphQLString 18 | }, 19 | manufacturer: { 20 | type: GraphQLString 21 | }, 22 | protocol: { 23 | type: GraphQLString 24 | }, 25 | description: { 26 | type: GraphQLString 27 | }, 28 | category: { 29 | type: GraphQLString 30 | }, 31 | parameters: { 32 | type: new GraphQLList( 33 | new GraphQLObjectType({ 34 | name: 'parameter', 35 | fields: { 36 | inputType: { 37 | type: GraphQLString 38 | }, 39 | required: { 40 | type: GraphQLBoolean 41 | }, 42 | id: { 43 | type: GraphQLString 44 | }, 45 | label: { 46 | type: GraphQLString 47 | }, 48 | regex: { 49 | type: GraphQLString 50 | }, 51 | tooltip: { 52 | type: GraphQLString 53 | }, 54 | placeholder: { 55 | type: GraphQLString 56 | }, 57 | items: { 58 | type: new GraphQLList( 59 | GraphQLJSONObject 60 | ) 61 | } 62 | } 63 | }) 64 | ) 65 | }, 66 | defaults: { 67 | type: new GraphQLList(GraphQLString) 68 | } 69 | } 70 | }) 71 | 72 | export default providerType 73 | -------------------------------------------------------------------------------- /packages/ui/src/View/Dashboard/ResourceSummary.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { useQuery } from 'urql' 3 | import { InlineLoading } from 'carbon-components-react' 4 | import { DataBase16, FolderDetails16, Application16, Apps16 } from '@carbon/icons-react' 5 | import GraphQLError from '../components/GraphQLError.jsx' 6 | import globalContext from '../../globalContext' 7 | 8 | const ResourceSummary = () => { 9 | const { contextRealm } = useContext(globalContext) 10 | const [result] = useQuery({ 11 | query: `query resourceSummary($realm: String, $core: String) { 12 | devices(realm: $realm, core: $core) { id } 13 | stacks(realm: $realm, core: $core) { id } 14 | panels(realm: $realm, core: $core) { id } 15 | controllers(realm: $realm, core: $core) { id } 16 | }`, 17 | variables: { realm: contextRealm.id, core: contextRealm.coreID }, 18 | pollInterval: 10000 19 | }) 20 | 21 | const offsetProp = { style: { transform: 'translate(0, 0.3em)' } } 22 | 23 | if (result.error) return 24 | if (result.fetching) return 25 | if (result.data) { 26 | return ( 27 | <> 28 | {result.data.devices.length} Device{result.data.devices.length !== 1 ? 's' : ''}
29 | {result.data.stacks.length} Stack{result.data.stacks.length !== 1 ? 's' : ''}
30 | {result.data.panels.length} Panel{result.data.panels.length !== 1 ? 's' : ''}
31 | {result.data.controllers.length} Controller{result.data.controllers.length !== 1 ? 's' : ''}
32 | 33 | ) 34 | } 35 | } 36 | 37 | export default ResourceSummary 38 | -------------------------------------------------------------------------------- /packages/link/src/network/schema.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | GraphQLObjectType, 4 | GraphQLInputObjectType, 5 | GraphQLString, 6 | GraphQLBoolean 7 | } from 'graphql' 8 | 9 | import { initGQLClient } from './graphql' 10 | import { config } from '../utils/config' 11 | 12 | const connectionType = new GraphQLObjectType({ 13 | name: 'connectionType', 14 | fields: { 15 | host: { 16 | type: GraphQLString 17 | }, 18 | status: { 19 | type: GraphQLBoolean 20 | }, 21 | https: { 22 | type: GraphQLBoolean 23 | } 24 | } 25 | }) 26 | 27 | const connectionInputType = new GraphQLInputObjectType({ 28 | name: 'connectionInputType', 29 | fields: { 30 | host: { 31 | type: GraphQLString 32 | }, 33 | status: { 34 | type: GraphQLBoolean 35 | }, 36 | https: { 37 | type: GraphQLBoolean 38 | } 39 | } 40 | }) 41 | 42 | var schema = new GraphQLSchema({ 43 | query: new GraphQLObjectType({ 44 | name: 'Queries', 45 | fields: { 46 | connection: { 47 | name: 'connection', 48 | description: 'Return connection information', 49 | type: connectionType, 50 | resolve: () => { 51 | return config.get('connection') 52 | } 53 | } 54 | } 55 | }), 56 | 57 | mutation: new GraphQLObjectType({ 58 | name: 'Mutations', 59 | fields: { 60 | connection: { 61 | name: 'connection', 62 | description: 'Update and reconnect', 63 | type: connectionType, 64 | args: { 65 | connection: { 66 | type: connectionInputType 67 | } 68 | }, 69 | resolve: (parent, args) => { 70 | config.set('connection', { ...args.connection }) 71 | initGQLClient(args.connection.host, args.connection.https) 72 | return 'OK' 73 | } 74 | } 75 | } 76 | }) 77 | }) 78 | 79 | export { schema } 80 | -------------------------------------------------------------------------------- /packages/core/src/providers/connectionProviders/ConnectionProviderOSC.js: -------------------------------------------------------------------------------- 1 | import osc from 'osc' 2 | import { devices } from '../../db' 3 | import log from '../../utils/log' 4 | import STATUS from '../../utils/statusEnum' 5 | import REGEX from '../../utils/regexEnum' 6 | 7 | class ConnectionProviderOSC { 8 | constructor (_device) { 9 | this.device = _device 10 | } 11 | 12 | static parameters = [ 13 | { 14 | inputType: 'textInput', 15 | id: 'host', 16 | label: 'Host', 17 | required: true, 18 | regex: REGEX.HOST, 19 | placeholder: 'Device Address' 20 | }, 21 | { 22 | inputType: 'numberInput', 23 | id: 'port', 24 | label: 'Port', 25 | required: true, 26 | regex: REGEX.PORT, 27 | placeholder: 'Device Port', 28 | tooltip: 'This device\'s connection provider needs to connect to a UDP OSC port' 29 | } 30 | ] 31 | 32 | init = () => { 33 | const deviceProxy = this.device 34 | this.client = new osc.UDPPort({ 35 | localAddress: '0.0.0.0', 36 | localPort: 0, 37 | metadata: true 38 | }) 39 | this.client.open() 40 | this.client.on('error', (error) => { 41 | log('error', `virtual/device/${deviceProxy.id} (${deviceProxy.label})`, error) 42 | }) 43 | devices.updateOne({ id: this.device.id }, { $set: { status: STATUS.OK } }) 44 | } 45 | 46 | destroy = (callback) => { 47 | if (this.client) { 48 | this.client.close() 49 | this.client = null 50 | } 51 | if (typeof callback === 'function') { 52 | callback() 53 | } 54 | } 55 | 56 | recreate = () => {} 57 | 58 | connectionProviderInterface = ({ address, args }) => { 59 | this.client.send( 60 | { 61 | address: address, 62 | args: args 63 | }, 64 | this.device.configuration.host, 65 | this.device.configuration.port 66 | ) 67 | } 68 | } 69 | 70 | export default ConnectionProviderOSC 71 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/panelTypes/panelType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLList, 5 | GraphQLInt 6 | } from 'graphql' 7 | 8 | import stackType from '../stackTypes/stackType' 9 | 10 | const panelType = new GraphQLObjectType({ 11 | name: 'panelType', 12 | description: 'A Panel is a virtual abstraction of a control interface', 13 | fields: { 14 | id: { 15 | type: GraphQLString 16 | }, 17 | label: { 18 | type: GraphQLString 19 | }, 20 | description: { 21 | type: GraphQLString 22 | }, 23 | realm: { 24 | type: GraphQLString 25 | }, 26 | core: { 27 | type: GraphQLString 28 | }, 29 | layoutType: { 30 | type: new GraphQLObjectType({ 31 | name: 'panelLayoutTypeType', 32 | fields: { 33 | id: { 34 | type: GraphQLString 35 | }, 36 | label: { 37 | type: GraphQLString 38 | } 39 | } 40 | }) 41 | }, 42 | layout: { 43 | type: new GraphQLObjectType({ 44 | name: 'panelLayoutType', 45 | fields: { 46 | id: { 47 | type: GraphQLString 48 | }, 49 | label: { 50 | type: GraphQLString 51 | }, 52 | rows: { 53 | type: GraphQLInt 54 | }, 55 | columns: { 56 | type: GraphQLInt 57 | } 58 | } 59 | }) 60 | }, 61 | buttons: { 62 | type: new GraphQLList( 63 | new GraphQLList( 64 | new GraphQLObjectType({ 65 | name: 'panelButtonType', 66 | fields: { 67 | row: { 68 | type: GraphQLInt 69 | }, 70 | column: { 71 | type: GraphQLInt 72 | }, 73 | stack: { 74 | type: stackType 75 | } 76 | } 77 | }) 78 | ) 79 | ) 80 | } 81 | } 82 | }) 83 | 84 | export default panelType 85 | -------------------------------------------------------------------------------- /packages/ui/src/View/Devices/queries.js: -------------------------------------------------------------------------------- 1 | const devicesQueryGQL = `query devices($realm: String, $core: String) { 2 | devices(realm: $realm, core: $core) { 3 | id 4 | label 5 | location 6 | description 7 | provider { 8 | id 9 | label 10 | } 11 | status 12 | } 13 | }` 14 | 15 | const deleteDeviceGQL = ` 16 | mutation deleteDevice($id: String!) { 17 | deleteDevice(id: $id) 18 | } 19 | ` 20 | 21 | const newDeviceQuery = ` 22 | query providers { 23 | providers { 24 | id 25 | label 26 | manufacturer 27 | protocol 28 | description 29 | category 30 | parameters { 31 | inputType 32 | required 33 | id 34 | label 35 | regex 36 | placeholder 37 | tooltip 38 | items 39 | } 40 | defaults 41 | } 42 | } 43 | ` 44 | 45 | const existingDeviceQuery = ` 46 | query deviceAndProviders($id: String) { 47 | providers { 48 | id 49 | label 50 | } 51 | device(id:$id){ 52 | id 53 | label 54 | location 55 | description 56 | status 57 | configuration { 58 | id 59 | value 60 | } 61 | provider { 62 | id 63 | label 64 | manufacturer 65 | protocol 66 | description 67 | category 68 | parameters { 69 | inputType 70 | required 71 | id 72 | label 73 | regex 74 | placeholder 75 | tooltip 76 | items 77 | } 78 | defaults 79 | } 80 | } 81 | } 82 | ` 83 | 84 | const deviceMutationGQL = `mutation device($device: deviceUpdate) { 85 | device(device: $device) { 86 | id 87 | } 88 | }` 89 | 90 | const enableDeviceMutationGQL = `mutation enableDevice($id: String) { 91 | enableDevice(id:$id) 92 | }` 93 | 94 | const disableDeviceMutationGQL = `mutation disableDevice($id: String) { 95 | disableDevice(id:$id) 96 | }` 97 | 98 | export { devicesQueryGQL, deleteDeviceGQL, newDeviceQuery, existingDeviceQuery, deviceMutationGQL, enableDeviceMutationGQL, disableDeviceMutationGQL } 99 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/controllerTypes/controllerInputType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLString, 4 | GraphQLInt 5 | } from 'graphql' 6 | 7 | const controllerType = new GraphQLInputObjectType({ 8 | name: 'controllerInputType', 9 | description: 'Controller Input Type for both hardware and bridged controllers', 10 | fields: { 11 | manufacturer: { 12 | type: GraphQLString 13 | }, 14 | model: { 15 | type: GraphQLString 16 | }, 17 | serial: { 18 | type: GraphQLString 19 | }, 20 | label: { 21 | type: GraphQLString 22 | }, 23 | description: { 24 | type: GraphQLString 25 | }, 26 | panel: { 27 | type: new GraphQLInputObjectType({ 28 | name: 'controllerPanelInputType', 29 | fields: { 30 | id: { 31 | type: GraphQLString 32 | }, 33 | label: { 34 | type: GraphQLString 35 | } 36 | } 37 | }) 38 | }, 39 | layout: { 40 | type: new GraphQLInputObjectType({ 41 | name: 'controllerLayoutInputType', 42 | fields: { 43 | id: { 44 | type: GraphQLString 45 | }, 46 | label: { 47 | type: GraphQLString 48 | }, 49 | rows: { 50 | type: GraphQLInt 51 | }, 52 | columns: { 53 | type: GraphQLInt 54 | } 55 | } 56 | }) 57 | }, 58 | type: { 59 | type: new GraphQLInputObjectType({ 60 | name: 'controllerLayoutTypeInputType', 61 | fields: { 62 | id: { 63 | type: GraphQLString 64 | }, 65 | label: { 66 | type: GraphQLString 67 | } 68 | } 69 | }) 70 | }, 71 | id: { 72 | type: GraphQLString 73 | }, 74 | status: { 75 | type: GraphQLString 76 | }, 77 | core: { 78 | type: GraphQLString 79 | }, 80 | realm: { 81 | type: GraphQLString 82 | } 83 | } 84 | }) 85 | 86 | export default controllerType 87 | -------------------------------------------------------------------------------- /packages/core/src/stacks/stackResolvers.js: -------------------------------------------------------------------------------- 1 | import { devices, stacks, tags } from '../db' 2 | import { updateStack, duplicateStack, deleteStack, executeStack, executeAction} from '.' 3 | 4 | const stackResolvers = { 5 | stacksQuery: (p, args) => new Promise((resolve, reject) => { 6 | const realmFilter = args.realm ? { realm: args.realm } : {} 7 | const coreFilter = args.core ? { core: args.core } : {} 8 | const resolveArray = [] 9 | stacks 10 | .find({ ...realmFilter, ...coreFilter }) 11 | .each((err, stack) => { 12 | if (err) { 13 | reject(err) 14 | } else if (stack == null) { 15 | resolve(resolveArray) 16 | } else { 17 | resolveArray.push({ 18 | ...stack, 19 | ...stack.actions.map(action => { 20 | action.device = devices.findOne({ id: action.device.id }) 21 | }), 22 | ...stack.tags?.map((tag, tagIndex) => { 23 | stack.tags[tagIndex] = tags.findOne({ id: tag, core: stack.core, realm: stack.realm }) 24 | }) 25 | }) 26 | } 27 | }) 28 | }), 29 | 30 | stackQuery: (parent, args) => { 31 | return new Promise((resolve, reject) => { 32 | stacks.findOne({ id: args.id }) 33 | .then(stack => resolve({ 34 | ...stack, 35 | ...stack.actions.map(action => { 36 | action.device = devices.findOne({ id: action.device.id }) 37 | }), 38 | ...stack.tags?.map((tag, tagIndex) => { 39 | stack.tags[tagIndex] = tags.findOne({ id: tag, core: stack.core, realm: stack.realm }) 40 | }) 41 | })) 42 | .catch(e => reject(e)) 43 | }) 44 | }, 45 | 46 | updateStackMutationResolver: (parent, args) => updateStack(args.stack), 47 | duplicateStackMutationResolver: (parent, args) => duplicateStack(args.id), 48 | deleteStackMutationResolver: (parent, args) => deleteStack(args.id), 49 | executeStackMutationResolver: (parent, args) => executeStack(args.id, args.controller), 50 | executeActionMutationResolver: (parent, args) => executeAction(args.action) 51 | } 52 | 53 | export { stackResolvers } 54 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/panelTypes/panelUpdateInputType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLString, 4 | GraphQLList, 5 | GraphQLInt 6 | } from 'graphql' 7 | 8 | import stackUpdateInputType from '../stackTypes/stackUpdateInputType' 9 | 10 | const panelUpdateInputType = new GraphQLInputObjectType({ 11 | name: 'panelUpdateInputType', 12 | description: 'A Panel is a virtual abstraction of a control interface', 13 | fields: { 14 | id: { 15 | type: GraphQLString 16 | }, 17 | label: { 18 | type: GraphQLString 19 | }, 20 | description: { 21 | type: GraphQLString 22 | }, 23 | realm: { 24 | type: GraphQLString 25 | }, 26 | core: { 27 | type: GraphQLString 28 | }, 29 | layoutType: { 30 | type: new GraphQLInputObjectType({ 31 | name: 'panelUpdateLayoutTypeInputType', 32 | fields: { 33 | id: { 34 | type: GraphQLString 35 | }, 36 | label: { 37 | type: GraphQLString 38 | } 39 | } 40 | }) 41 | }, 42 | layout: { 43 | type: new GraphQLInputObjectType({ 44 | name: 'panelUpdateLayoutInputType', 45 | fields: { 46 | id: { 47 | type: GraphQLString 48 | }, 49 | label: { 50 | type: GraphQLString 51 | }, 52 | rows: { 53 | type: GraphQLInt 54 | }, 55 | columns: { 56 | type: GraphQLInt 57 | } 58 | } 59 | }) 60 | }, 61 | buttons: { 62 | type: new GraphQLList( 63 | new GraphQLList( 64 | new GraphQLInputObjectType({ 65 | name: 'panelUpdateButtonInputType', 66 | fields: { 67 | row: { 68 | type: GraphQLInt 69 | }, 70 | column: { 71 | type: GraphQLInt 72 | }, 73 | stack: { 74 | type: stackUpdateInputType 75 | } 76 | } 77 | }) 78 | ) 79 | ) 80 | } 81 | } 82 | }) 83 | 84 | export default panelUpdateInputType 85 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.3.0", 4 | "description": "The configuration interface component of the Boreal Director orchestration suite", 5 | "main": "src/index.js", 6 | "repository": "https://github.com/borealsystems/Director", 7 | "author": "Oliver Herrmann", 8 | "license": "GPL-3.0", 9 | "scripts": { 10 | "build": "webpack --mode production src", 11 | "analyze": "source-map-explorer 'dist/*.js'", 12 | "dev": "webpack-dev-server" 13 | }, 14 | "devDependencies": { 15 | "@babel/cli": "^7.8.4", 16 | "@babel/core": "^7.8.4", 17 | "@babel/node": "^7.8.4", 18 | "@babel/preset-env": "^7.8.4", 19 | "@babel/preset-react": "^7.8.3", 20 | "babel-eslint": "^10.1.0", 21 | "babel-loader": "^8.0.6", 22 | "css-loader": "^3.4.2", 23 | "eslint": "^6.8.0", 24 | "eslint-config-standard": "^14.1.0", 25 | "eslint-plugin-import": "^2.20.1", 26 | "eslint-plugin-node": "^11.0.0", 27 | "eslint-plugin-promise": "^4.2.1", 28 | "eslint-plugin-react": "^7.18.3", 29 | "eslint-plugin-react-hooks": "^4.1.2", 30 | "eslint-plugin-standard": "^4.0.1", 31 | "file-loader": "^5.1.0", 32 | "node-sass": "^4.13.1", 33 | "sass-loader": "^8.0.2", 34 | "source-map-explorer": "^2.5.0", 35 | "style-loader": "^1.1.3", 36 | "svg-url-loader": "^4.0.0", 37 | "webpack": "^4.41.6", 38 | "webpack-cli": "^3.3.11" 39 | }, 40 | "dependencies": { 41 | "@carbon/grid": "^10.8.3", 42 | "@carbon/icons-react": "^10.8.2", 43 | "@carbon/themes": "^10.17.0", 44 | "@hot-loader/react-dom": "16.12.0", 45 | "carbon-components": "^10.9.3", 46 | "carbon-components-react": "^7.9.3", 47 | "carbon-icons": "^7.0.7", 48 | "formik": "^2.1.5", 49 | "graphql": "^15.3.0", 50 | "lodash": "^4.17.15", 51 | "prop-types": "^15.7.2", 52 | "react": "^16.12.0", 53 | "react-dom": "^16.12.0", 54 | "react-hot-loader": "^4.12.19", 55 | "react-markdown": "^4.3.1", 56 | "react-router-dom": "^5.1.2", 57 | "shortid": "^2.2.15", 58 | "subscriptions-transport-ws": "^0.9.18", 59 | "urql": "^1.9.0", 60 | "webpack-dev-server": "^3.10.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/ui/src/View/Devices/ProviderTile.jsx: -------------------------------------------------------------------------------- 1 | import { CheckmarkOutline24 } from '@carbon/icons-react' 2 | import { AspectRatio, Column, Grid, Row, ClickableTile } from 'carbon-components-react' 3 | import React from 'react' 4 | 5 | const ProviderTile = ({ providerDescription, onClick, currentDevice, disabled }) => { 6 | const isCurrentProvider = currentDevice?.provider?.id === providerDescription.id 7 | const labelProps = { style: { fontWeight: 300 } } 8 | if (!providerDescription.isPadding) { 9 | return ( 10 | {} : () => { onClick(providerDescription) }}> 11 | 12 | 13 | 14 | 15 |
{providerDescription.label}
16 |
17 | 18 | { isCurrentProvider && } 19 | 20 |
21 |
22 | 23 | 24 | {providerDescription.description} 25 | 26 | 27 | 28 | 29 | Manufacturer 30 | {providerDescription.manufacturer} 31 | 32 | 33 | Protocol 34 | {providerDescription.protocol} 35 | 36 | 37 | Category 38 | {providerDescription.category} 39 | 40 | 41 |
42 |
43 |
44 | ) 45 | } else return null 46 | } 47 | 48 | export default ProviderTile 49 | -------------------------------------------------------------------------------- /packages/ui/src/View/Executer/DeviceTile.jsx: -------------------------------------------------------------------------------- 1 | import { AspectRatio, Column, Grid, Row, ClickableTile } from 'carbon-components-react' 2 | import { CheckmarkOutline24 } from '@carbon/icons-react' 3 | import PropTypes from 'prop-types' 4 | import React from 'react' 5 | 6 | const DeviceTile = ({ device, onClick, selectedDevice }) => { 7 | const isCurrentDevice = device.id === selectedDevice?.id 8 | const labelProps = { style: { fontWeight: 300 } } 9 | if (!device.isPadding) { 10 | return ( 11 | onClick(device)}> 12 | 13 | 14 | 15 | 16 |
{device.label}
17 |
18 | 19 | { isCurrentDevice && } 20 | 21 |
22 | 23 | 24 | {device.id ?? 'NO ID'} 25 | 26 | 27 | 28 | 29 | {device.location} 30 | 31 | 32 |
33 | 34 | 35 | {device.description} 36 | 37 | 38 | 39 | 40 | Provider 41 | {device.provider.label} 42 | 43 | 44 |
45 |
46 |
47 | ) 48 | } else return null 49 | } 50 | 51 | DeviceTile.propTypes = { 52 | device: PropTypes.object, 53 | selectedDevice: PropTypes.object, 54 | onClick: PropTypes.func 55 | } 56 | 57 | export default DeviceTile 58 | -------------------------------------------------------------------------------- /packages/ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const port = process.env.PORT || 3000 5 | 6 | module.exports = { 7 | mode: 'development', 8 | 9 | entry: [ 10 | // 'webpack-dev-server/client?http://0.0.0.0:3000', 11 | './src/index.js' 12 | ], 13 | 14 | output: { 15 | path: path.resolve(__dirname, 'dist'), 16 | publicPath: '/dist/', 17 | filename: 'bundle.js' 18 | }, 19 | 20 | plugins: [ 21 | new webpack.HotModuleReplacementPlugin(), 22 | new webpack.SourceMapDevToolPlugin({ 23 | filename: 'sourcemaps/[file].map' 24 | }) 25 | ], 26 | 27 | resolve: { 28 | alias: { 29 | 'react-dom': '@hot-loader/react-dom' 30 | } 31 | }, 32 | 33 | optimization: { 34 | usedExports: true 35 | }, 36 | 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.css$/i, 41 | use: ['style-loader', 'css-loader'] 42 | }, 43 | { 44 | test: /\.(js|jsx)$/, 45 | exclude: /(node_modules|bower_components)/, 46 | loader: ['react-hot-loader/webpack', 'babel-loader'] 47 | }, 48 | { 49 | test: /\.(png|jpe?g|gif)$/i, 50 | use: [ 51 | { 52 | loader: 'file-loader' 53 | } 54 | ] 55 | }, 56 | { 57 | test: /\.svg$/, 58 | use: [ 59 | { 60 | loader: 'svg-url-loader', 61 | options: { 62 | limit: 10000 63 | } 64 | } 65 | ] 66 | }, 67 | { 68 | test: /\.s[ac]ss$/i, 69 | use: [ 70 | 'style-loader', 71 | 'css-loader', 72 | 'sass-loader' 73 | ] 74 | } 75 | ] 76 | }, 77 | 78 | devServer: { 79 | host: '0.0.0.0', 80 | port: port, 81 | historyApiFallback: true, 82 | open: false, 83 | disableHostCheck: true, 84 | contentBase: path.join(__dirname, '/src/public/'), 85 | publicPath: 'http://localhost:3000/dist/', 86 | proxy: { 87 | '/graphql': { 88 | target: 'ws://localhost:3001', 89 | changeOrigin: true, 90 | ws: true 91 | } 92 | }, 93 | hot: true, 94 | hotOnly: true 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/ui/src/View/Landing/Landing.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { useHistory } from 'react-router-dom' 5 | 6 | import { 7 | ComboBox, 8 | Grid, 9 | Row, 10 | Column 11 | } from 'carbon-components-react' 12 | 13 | import lost from './lost.svg' 14 | 15 | const Landing = ({ realms, realm, setRealm }) => { 16 | const history = useHistory() 17 | // if (realm.core) history.push(`/${realm.core.id}/${realm.realm.id}/`) 18 | return ( 19 | 20 | 21 | 22 |


23 | 24 |

Lost?

25 |

26 | { realms.length === 0 && 27 | 28 |

It looks like you can't find any realms

29 |
30 | } 31 | { realms.length !== 0 && 32 | 33 | { return item.id === 'ROOT' ? item.coreLabel : `${item.coreLabel} / ${item.label}` }} 44 | onChange={(event) => { 45 | setRealm(event.selectedItem) 46 | history.push(`/cores/${event.selectedItem.coreID}/realms/${event.selectedItem.id}/`) 47 | }} 48 | /> 49 | 50 | } 51 |
52 | 53 | If you arrived here by a broken link, please submit a bug report. 54 |
55 |
56 | 57 | 58 | 59 |
60 |
61 | ) 62 | } 63 | 64 | Landing.propTypes = { 65 | realms: PropTypes.array, 66 | realm: PropTypes.object, 67 | setRealm: PropTypes.func 68 | } 69 | 70 | export default Landing 71 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Boreal Director 2 | A Broadcast Orchestration Platform. 3 | 4 | Effective Jan 1st 2022, Boreal Director is no longer actively maintained. 5 | 6 | Due to changes in my employment (funnily enough, as a result of this project) 7 | I no longer have the time nor motivation to continue in depth development on it. 8 | 9 | I will attempt to continue occasional maintenance with security and stability updates, 10 | but will not be adding any features for the foreseeable future, the whole 11 | thing may be revived at some point in the future when circumstances change 12 | but don't count on it. 13 | 14 | Feel free to hack it and use it to learn about React/NodeJS and Broadcast Control 15 | and hopefully it can help you gain a better understanding of the broadcast world 16 | like it did for me. I got a good job out of it so while it hasn't achieved it's 17 | original goal of changing how we think of broadcast control, it did change how I 18 | get to interact with and solve problems in the work I do, so it's not all bad. 19 | 20 | Huge thank you to everyone who helped with design, development, testing, 21 | and feedback. Without you I would have had no hope of learning as much as I did 22 | nor had the chance to develop something that could be practically used in a 23 | large scale production. 24 | 25 | Thanks for coming on the journey and I'm sorry it didn't last very long. 26 | Keep doing great things, Oliver Herrmann 27 | 28 | ## License 29 | Boreal Director is licensed under the GPLv3 License. 30 | Copyright (C) 2020 - 2022 Boreal Systems - Oliver Herrmann, https://boreal.systems 31 | 32 | This program is free software: you can redistribute it and/or modify 33 | it under the terms of the GNU General Public License as published by 34 | the Free Software Foundation, either version 3 of the License, or 35 | (at your option) any later version. 36 | 37 | This program is distributed in the hope that it will be useful, 38 | but WITHOUT ANY WARRANTY; without even the implied warranty of 39 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 40 | GNU General Public License for more details. 41 | 42 | You should have received a copy of the GNU General Public License 43 | along with this program. If not, see . 44 | -------------------------------------------------------------------------------- /packages/ui/src/View/components/SidebarNav.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { SideNav, SideNavItems } from 'carbon-components-react' 3 | import { Application24, Dashboard24, DataBase24, DeploymentPolicy24, FolderDetails24, Apps24, Rocket24, Workspace24, FunctionMath24, Tag24 } from '@carbon/icons-react' 4 | import globalContext from '../../globalContext' 5 | import SideNavLink from './SideNavLink.jsx' 6 | import useWindowDimensions from '../hooks/useWindowDimensions' 7 | 8 | const SidebarNav = ({ isActive }) => { 9 | const { contextRealm } = useContext(globalContext) 10 | const { width } = useWindowDimensions(); 11 | 12 | return ( 13 | 1055}> 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 | 27 | 28 |
29 |
30 | ) 31 | } 32 | 33 | export default SidebarNav 34 | -------------------------------------------------------------------------------- /packages/ui/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { hot } from 'react-hot-loader/root' 3 | import { Provider, Client, defaultExchanges, subscriptionExchange } from 'urql' 4 | import { SubscriptionClient } from 'subscriptions-transport-ws' 5 | import { BrowserRouter as Router } from 'react-router-dom' 6 | import BorealDirector from './View/Index.jsx' 7 | import globalContext from './globalContext' 8 | import './theme.scss' 9 | 10 | const subscriptionClient = new SubscriptionClient(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.hostname}:${window.location.port}/graphql`, { reconnect: true }) 11 | 12 | const client = new Client({ 13 | url: '/graphql', 14 | requestPolicy: 'network-only', 15 | maskTypename: true, 16 | exchanges: [ 17 | ...defaultExchanges, 18 | subscriptionExchange({ 19 | forwardSubscription (operation) { 20 | return subscriptionClient.request(operation) 21 | } 22 | }) 23 | ] 24 | }) 25 | 26 | const App = () => { 27 | const [contextRealm, _setContextRealm] = useState(JSON.parse(localStorage.getItem('contextRealm')) ?? {}) 28 | const setContextRealm = (event) => { 29 | localStorage.setItem('contextRealm', JSON.stringify(event)) 30 | _setContextRealm(event) 31 | } 32 | 33 | const darkClass = 'dx--dark' 34 | const lightClass = 'dx--light' 35 | 36 | const [theme, _setTheme] = useState(localStorage.getItem('uiTheme') ?? darkClass) 37 | 38 | const toggleTheme = () => { 39 | switch (theme) { 40 | case darkClass : 41 | document.body.classList.remove(darkClass) 42 | document.body.classList.add(lightClass) 43 | localStorage.setItem('uiTheme', lightClass) 44 | _setTheme(lightClass) 45 | break 46 | case lightClass : 47 | document.body.classList.remove(lightClass) 48 | document.body.classList.add(darkClass) 49 | localStorage.setItem('uiTheme', darkClass) 50 | _setTheme(darkClass) 51 | break 52 | } 53 | } 54 | 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ) 64 | } 65 | 66 | export default hot(App) 67 | -------------------------------------------------------------------------------- /packages/ui/src/View/Shotbox/ShotboxPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Button, Grid, Row, Column } from 'carbon-components-react' 4 | import { useMutation } from 'urql' 5 | 6 | const getEnabledProps = (button) => { 7 | if (button.stack) { 8 | return { kind: 'primary' } 9 | } else { 10 | return { disabled: true, kind: 'secondary' } 11 | } 12 | } 13 | 14 | const ShotboxPanel = ({ inline, panel, controller }) => { 15 | const executeStackMutationGQL = `mutation executeStack($executeID: String, $controller: String) { 16 | executeStack(id: $executeID, controller: $controller) 17 | }` 18 | 19 | var [, executeStackMutation] = useMutation(executeStackMutationGQL) 20 | return ( 21 | 22 | { !inline && 23 | 24 | 25 |

{panel.label}

26 |

27 |
28 |
29 | } 30 |
31 | { panel.buttons !== undefined && panel.buttons.map((row, rowIndex) => { 32 | return ( 33 | 34 | { row.map((button, buttonIndex) => ( 35 | 36 | 48 | 49 | )) 50 | } 51 | 52 | ) 53 | })} 54 |
55 |
56 |
57 | ) 58 | } 59 | 60 | ShotboxPanel.propTypes = { 61 | panel: PropTypes.object, 62 | inline: PropTypes.bool, 63 | controller: PropTypes.object 64 | } 65 | 66 | export default ShotboxPanel 67 | -------------------------------------------------------------------------------- /packages/core/src/providers/connectionProviders/ConnectionProviderTCP.js: -------------------------------------------------------------------------------- 1 | import net from 'net' 2 | import { devices } from '../../db' 3 | import log from '../../utils/log' 4 | import STATUS from '../../utils/statusEnum' 5 | import REGEX from '../../utils/regexEnum' 6 | 7 | class ConnectionProviderTCP { 8 | constructor (_device) { 9 | this.device = _device 10 | } 11 | 12 | static parameters = [ 13 | { 14 | inputType: 'textInput', 15 | id: 'host', 16 | label: 'Host', 17 | required: true, 18 | regex: REGEX.HOST, 19 | placeholder: 'Device Host' 20 | }, 21 | { 22 | inputType: 'numberInput', 23 | id: 'port', 24 | label: 'Port', 25 | required: true, 26 | regex: REGEX.PORT, 27 | tooltip: 'This device\'s connection provider needs to connect to a port via TCP' 28 | } 29 | ] 30 | 31 | init = () => { 32 | this.socket = net.createConnection(this.device.configuration.port, this.device.configuration.host) 33 | this.socket.setKeepAlive(true, 0) 34 | 35 | this.socket.on('connect', () => { 36 | log('info', `virtual/device/${this.device.id} (${this.device.label})`, 'Socket Connected') 37 | devices.updateOne({ id: this.device.id }, { $set: { status: STATUS.OK } }) 38 | }) 39 | 40 | this.socket.on('error', (error) => { 41 | log('error', `virtual/device/${this.device.id} (${this.device.label})`, `${error}`) 42 | devices.updateOne({ id: this.device.id }, { $set: { status: STATUS.ERROR } }) 43 | }) 44 | 45 | this.socket.on('close', () => { 46 | devices.updateOne({ id: this.device.id }, { $set: { status: STATUS.CLOSED } }) 47 | switch (this.doNotRecreate) { 48 | case true: 49 | break 50 | case false: 51 | log('error', `virtual/device/${this.device.id} (${this.device.label})`, 'Socket Closed, Reconnecting in 10 Seconds') 52 | setTimeout(() => this.recreate(), 10000) 53 | } 54 | }) 55 | } 56 | 57 | destroy = (callback) => { 58 | log('info', `virtual/device/${this.device.id} (${this.device.label})`, 'Destroying Instance') 59 | this.doNotRecreate = true 60 | if (this.socket) { 61 | this.socket.destroy() 62 | } 63 | if (typeof callback === 'function') { 64 | callback() 65 | } 66 | } 67 | 68 | recreate = () => { 69 | this.destroy() 70 | this.init() 71 | } 72 | 73 | connectionProviderInterface = ({ message }) => { 74 | this.socket.write(message) 75 | } 76 | } 77 | 78 | export default ConnectionProviderTCP 79 | -------------------------------------------------------------------------------- /packages/core/src/providers/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import log from '../utils/log.js' 4 | const SerialPort = require('serialport') 5 | 6 | const providers = [] 7 | 8 | const providerInterfaces = [] 9 | 10 | const providerInitMethods = [] 11 | 12 | let isSerial = false 13 | 14 | const initProviders = () => { 15 | return new Promise((resolve, reject) => { 16 | SerialPort.list().then(d => isSerial = d.length > 0) 17 | log('info', 'core/providers', 'Loading Providers') 18 | fs.readdir(path.resolve(__dirname, './protocolProviders'), (err, files) => { 19 | if (err) log('error', 'core/providers', err) 20 | var counter = files.length 21 | files.forEach(file => { 22 | import(`./protocolProviders/${file}`) 23 | .then((module) => { 24 | providers.push(module.default.providerRegistration) 25 | log('info', 'core/providers', `Loaded ${module.default.providerRegistration.id} (${module.default.providerRegistration.label})`) 26 | counter-- 27 | if (counter === 0) { 28 | loadDeviceProviders() 29 | } 30 | }) 31 | .catch(err => log('error', 'core/providers', err)) 32 | }) 33 | }) 34 | 35 | const loadDeviceProviders = () => { 36 | fs.readdir(path.resolve(__dirname, './deviceProviders'), (err, directories) => { 37 | if (err) log('error', 'core/providers', err) 38 | var counter = directories.length 39 | directories.forEach(directory => { 40 | import(`./deviceProviders/${directory}`) 41 | .then((module) => { 42 | if (module.default.providerRegistration.protocol === 'Serial' && !isSerial) { 43 | log('info', 'core/providers', `Skipping ${module.default.providerRegistration.id} (${module.default.providerRegistration.label}) as this system has no Serial Interfaces`) 44 | } 45 | else { 46 | providers.push(module.default.providerRegistration) 47 | log('info', 'core/providers', `Loaded ${module.default.providerRegistration.id} (${module.default.providerRegistration.label})`) 48 | } 49 | counter-- 50 | if (counter === 0) { 51 | resolve() 52 | } 53 | }) 54 | .catch(err => log('error', 'core/providers', err)) 55 | }) 56 | }) 57 | } 58 | }) 59 | } 60 | 61 | export { initProviders, providerInterfaces, providerInitMethods, providers } 62 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/stackTypes/stackType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLList, 5 | GraphQLInt 6 | } from 'graphql' 7 | import GraphQLJSON from 'graphql-type-json' 8 | import deviceType from '../deviceTypes/deviceType' 9 | import globalColourType from '../coreTypes/globalColourType' 10 | import tagType from '../tagTypes/tagType' 11 | 12 | const stackType = new GraphQLObjectType({ 13 | name: 'stackUpdateType', 14 | description: 'A Stack is a group of actions that can be triggered at once or sequentially by a controller', 15 | fields: { 16 | id: { 17 | type: GraphQLString 18 | }, 19 | label: { 20 | type: GraphQLString 21 | }, 22 | panelLabel: { 23 | type: GraphQLString 24 | }, 25 | description: { 26 | type: GraphQLString 27 | }, 28 | realm: { 29 | type: GraphQLString 30 | }, 31 | core: { 32 | type: GraphQLString 33 | }, 34 | colour: { 35 | type: globalColourType 36 | }, 37 | tags: { 38 | type: new GraphQLList(tagType) 39 | }, 40 | actions: { 41 | type: new GraphQLList( 42 | new GraphQLObjectType({ 43 | name: 'stackActionType', 44 | description: 'An action is something that happens on a device or piece of software', 45 | fields: { 46 | id: { 47 | type: GraphQLString 48 | }, 49 | device: { 50 | type: deviceType 51 | }, 52 | delay: { 53 | type: GraphQLInt 54 | }, 55 | providerFunction: { 56 | type: new GraphQLObjectType({ 57 | name: 'stackDeviceProviderFunctionType', 58 | fields: { 59 | id: { 60 | type: GraphQLString 61 | }, 62 | label: { 63 | type: GraphQLString 64 | } 65 | } 66 | }) 67 | }, 68 | parameters: { 69 | type: new GraphQLList( 70 | new GraphQLObjectType({ 71 | name: 'stackActionParametersType', 72 | fields: { 73 | id: { 74 | type: GraphQLString 75 | }, 76 | value: { 77 | type: GraphQLJSON 78 | } 79 | } 80 | }) 81 | ) 82 | } 83 | } 84 | }) 85 | ) 86 | } 87 | } 88 | }) 89 | 90 | export default stackType 91 | -------------------------------------------------------------------------------- /packages/link/src/streamdeck/utils/buttonLUT.js: -------------------------------------------------------------------------------- 1 | // Yes I know this is stupid 2 | 3 | const buttonLUT = { 4 | elgato: { 5 | mini: { 6 | forward: [ 7 | [0, 1, 2], 8 | [3, 4, 5] 9 | ], 10 | reverse: [ 11 | { row: 0, column: 0 }, 12 | { row: 0, column: 1 }, 13 | { row: 0, column: 2 }, 14 | { row: 1, column: 0 }, 15 | { row: 1, column: 1 }, 16 | { row: 1, column: 2 } 17 | ] 18 | }, 19 | original: { 20 | forward: [ 21 | [0, 1, 2, 3, 4], 22 | [5, 6, 7, 8, 9], 23 | [10, 11, 12, 13, 14] 24 | ], 25 | reverse: [ 26 | { row: 0, column: 0 }, 27 | { row: 0, column: 1 }, 28 | { row: 0, column: 2 }, 29 | { row: 0, column: 3 }, 30 | { row: 0, column: 4 }, 31 | { row: 1, column: 0 }, 32 | { row: 1, column: 1 }, 33 | { row: 1, column: 2 }, 34 | { row: 1, column: 3 }, 35 | { row: 1, column: 4 }, 36 | { row: 2, column: 0 }, 37 | { row: 2, column: 1 }, 38 | { row: 2, column: 2 }, 39 | { row: 2, column: 3 }, 40 | { row: 2, column: 4 } 41 | ] 42 | }, 43 | xl: { 44 | forward: [ 45 | [0, 1, 2, 3, 4, 5, 6, 7], 46 | [8, 9, 10, 11, 12, 13, 14, 15], 47 | [16, 17, 18, 19, 20, 21, 22, 23], 48 | [24, 25, 26, 27, 28, 29, 30, 31] 49 | ], 50 | reverse: [ 51 | { row: 0, column: 0 }, 52 | { row: 0, column: 1 }, 53 | { row: 0, column: 2 }, 54 | { row: 0, column: 3 }, 55 | { row: 0, column: 4 }, 56 | { row: 0, column: 5 }, 57 | { row: 0, column: 6 }, 58 | { row: 0, column: 7 }, 59 | { row: 1, column: 0 }, 60 | { row: 1, column: 1 }, 61 | { row: 1, column: 2 }, 62 | { row: 1, column: 3 }, 63 | { row: 1, column: 4 }, 64 | { row: 1, column: 5 }, 65 | { row: 1, column: 6 }, 66 | { row: 1, column: 7 }, 67 | { row: 2, column: 0 }, 68 | { row: 2, column: 1 }, 69 | { row: 2, column: 2 }, 70 | { row: 2, column: 3 }, 71 | { row: 2, column: 4 }, 72 | { row: 2, column: 5 }, 73 | { row: 2, column: 6 }, 74 | { row: 2, column: 7 }, 75 | { row: 3, column: 0 }, 76 | { row: 3, column: 1 }, 77 | { row: 3, column: 2 }, 78 | { row: 3, column: 3 }, 79 | { row: 3, column: 4 }, 80 | { row: 3, column: 5 }, 81 | { row: 3, column: 6 }, 82 | { row: 3, column: 7 } 83 | ] 84 | } 85 | } 86 | } 87 | 88 | export default buttonLUT 89 | -------------------------------------------------------------------------------- /packages/ui/src/View/Executer/undraw_No_data_re_kwbl.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/src/main.js: -------------------------------------------------------------------------------- 1 | import { initDevices, cleanupDevices } from './devices' 2 | import { initProviders } from './providers' 3 | import { initApollo, initRossTalk} from './network' 4 | import { initBridges } from './bridges' 5 | import { initDB, cores, devices } from './db' 6 | import STATUS from './utils/statusEnum' 7 | import log from './utils/log' 8 | import fs from 'fs' 9 | 10 | require('dotenv').config() 11 | 12 | initApollo() 13 | initRossTalk() 14 | initBridges() 15 | initDB() 16 | .then(() => { 17 | cores.countDocuments({}, (err, count) => { 18 | if (err) log('error', 'core', err) 19 | if (count === 0) { 20 | log('info', 'core', 'Initialising Database') 21 | cores.insertOne( 22 | { 23 | id: process.env.DIRECTOR_CORE_ID, 24 | label: process.env.DIRECTOR_CORE_LABEL, 25 | helpdeskVisable: false, 26 | realms: [ 27 | { 28 | id: 'ROOT', 29 | label: 'Root', 30 | description: `The Root Realm on ${process.env.DIRECTOR_CORE_LABEL}`, 31 | notes: 'Welcome to Director!\n\nAdministrators can edit this note via the Realm Configuration page.\n\nYou should probably include some useful notes here, like who manages and administers this system, and a link to your internal helpdesk or ticketing system for users having problems.' 32 | } 33 | ] 34 | } 35 | ) 36 | devices.insertOne( 37 | { 38 | core: process.env.DIRECTOR_CORE_ID, 39 | realm: 'ROOT', 40 | id: `CORE-${process.env.DIRECTOR_CORE_ID}`, 41 | label: process.env.DIRECTOR_CORE_LABEL, 42 | location: 'The Void', 43 | provider: { id: 'ProtocolProviderBorealDirector', label: 'BorealDirector' }, 44 | enabled: true, 45 | status: STATUS.UNKNOWN, 46 | description: `The virtual device for Core ${process.env.DIRECTOR_CORE_ID}` 47 | } 48 | ) 49 | } 50 | }) 51 | }) 52 | .then(() => initProviders()) 53 | .then(() => initDevices()) 54 | .catch(e => log('error', 'core', e)) 55 | 56 | process.on('SIGINT', () => { 57 | fs.appendFileSync('logs.txt', '========= TERMINATING =========\r\n') 58 | cleanupDevices() 59 | .then(() => log('warn', 'core', '========= TERMINATING =========')) 60 | .then(() => process.exit()) 61 | }) 62 | 63 | process.on('SIGHUP', () => { 64 | fs.appendFileSync('logs.txt', '========= TERMINATING =========\r\n') 65 | cleanupDevices() 66 | .then(() => log('warn', 'core', '========= TERMINATING =========')) 67 | .then(() => process.exit()) 68 | }) 69 | -------------------------------------------------------------------------------- /packages/core/src/providers/connectionProviders/ConnectionProviderSerial.js: -------------------------------------------------------------------------------- 1 | import { devices } from '../../db' 2 | import log from '../../utils/log' 3 | import STATUS from '../../utils/statusEnum' 4 | const SerialPort = require('serialport') 5 | const Readline = SerialPort.parsers.Readline 6 | 7 | class ConnectionProviderSerial { 8 | constructor (_device) { 9 | this.device = _device 10 | } 11 | 12 | static getItems = () => new Promise(resolve => { 13 | SerialPort.list().then(data => resolve([...data.map((port, index) => ({ id: index, label: port.path, ...port }))])) 14 | }) 15 | 16 | static parameters = [ 17 | { 18 | inputType: 'comboBox', 19 | id: 'port', 20 | label: 'Serial Port', 21 | required: true, 22 | placeholder: 'Select a Serial Port', 23 | tooltip: 'This device\'s connection provider needs to connect via a local serial port.', 24 | items: this.getItems() 25 | } 26 | ] 27 | 28 | init = () => { 29 | this.serialport = new SerialPort(this.device.configuration.port.path, { baudRate: 9600 }, error => { 30 | if (error) { 31 | log('error', `virtual/device/${this.device.id} (${this.device.label})`, `${error}`) 32 | devices.updateOne({ id: this.device.id }, { $set: { status: STATUS.ERROR } }) 33 | } 34 | }) 35 | this.parser = new Readline() 36 | this.serialport.pipe(this.parser) 37 | this.serialport.write('TERMINAL OFF\r\n', (error) => { 38 | if (error) { 39 | log('error', `virtual/device/${this.device.id} (${this.device.label})`, `${error}`) 40 | devices.updateOne({ id: this.device.id }, { $set: { status: STATUS.CONNECTING } }) 41 | } else { 42 | devices.updateOne({ id: this.device.id }, { $set: { status: STATUS.OK } }) 43 | } 44 | }) 45 | 46 | this.serialport.on('error', error => { 47 | log('error', `virtual/device/${this.device.id} (${this.device.label})`, `${error}`) 48 | devices.updateOne({ id: this.device.id }, { $set: { status: STATUS.ERROR } }) 49 | }) 50 | } 51 | 52 | destroy = (callback) => { 53 | log('info', `virtual/device/${this.device.id} (${this.device.label})`, 'Destroying Instance') 54 | } 55 | 56 | recreate = () => { 57 | this.destroy() 58 | this.init() 59 | } 60 | 61 | connectionProviderInterface = ({ message }) => { 62 | this.serialport.write(`${message}\r\n`, (error) => { 63 | if (error) { 64 | log('error', `virtual/device/${this.device.id} (${this.device.label})`, `${error}`) 65 | devices.updateOne({ id: this.device.id }, { $set: { status: STATUS.ERROR } }) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | export default ConnectionProviderSerial 72 | -------------------------------------------------------------------------------- /packages/link/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link", 3 | "version": "0.0.0", 4 | "description": "The link application for the Boreal Director orchestation suite", 5 | "main": "src/link.js", 6 | "bin": "build/link.js", 7 | "repository": "https://github.com/borealsystems/Director", 8 | "author": "Oliver Herrmann", 9 | "license": "GPL-3.0", 10 | "scripts": { 11 | "release": "yarn workspace link run webpack && yarn workspace link run build && yarn workspace link run package && yarn workspace link run copy-builds && rm -rf build", 12 | "copy-builds": "sh copy-builds.sh", 13 | "copy-deps": "sh copy-deps.sh", 14 | "package": "rm -rf dist && pkg package.json --out-path dist && yarn workspace link run copy-deps", 15 | "build": "rm -rf build && babel src/ -d build/ && cp -r src/network/ui/public build/network/ui/ && cp src/network/ui/dist/*.svg build/network/ui/dist/ && rm -rf build/network/ui/src", 16 | "webpack": "rm -rf src/network/ui/dist && webpack --mode production src/network/ui/src/index.jsx", 17 | "dev": "NODE_ENV=development nodemon --signal SIGHUP --exec babel-node src/link.js" 18 | }, 19 | "pkg": { 20 | "scripts": "build/**/*.js", 21 | "assets": [ 22 | "build/network/ui/dist/*", 23 | "build/network/ui/public/*", 24 | "../../node_modules/systray/traybin/tray_darwin_release" 25 | ] 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.8.4", 29 | "@babel/core": "^7.8.4", 30 | "@babel/node": "^7.8.4", 31 | "@babel/preset-env": "^7.8.4", 32 | "babel-eslint": "^10.1.0", 33 | "babel-loader": "^8.0.6", 34 | "eslint": "^6.8.0", 35 | "eslint-config-standard": "^14.1.0", 36 | "eslint-plugin-import": "^2.20.1", 37 | "eslint-plugin-node": "^11.0.0", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-react": "^7.18.3", 40 | "eslint-plugin-standard": "^4.0.1", 41 | "nodemon": "^2.0.2", 42 | "pkg": "^4.4.9" 43 | }, 44 | "dependencies": { 45 | "@julusian/jpeg-turbo": "^0.5.6", 46 | "apollo-client": "^2.6.10", 47 | "babel-preset-es2015": "^6.24.1", 48 | "chalk": "^3.0.0", 49 | "debug": "^4.1.1", 50 | "elgato-stream-deck": "^3.3.1", 51 | "express-graphql": "^0.11.0", 52 | "fetch": "^1.1.0", 53 | "graphql": "^14.6.0", 54 | "graphql-request": "1.8.2", 55 | "isomorphic-unfetch": "^3.0.0", 56 | "jimp": "^0.16.1", 57 | "lodash": "^4.17.15", 58 | "nicely-format": "^1.1.0", 59 | "open": "^7.1.0", 60 | "shortid": "^2.2.15", 61 | "skia-canvas": "^0.9.19", 62 | "smart-promisify": "^1.0.5", 63 | "subscriptions-transport-ws": "^0.9.17", 64 | "systray": "^1.0.5", 65 | "urql": "^1.9.8", 66 | "websocket": "^1.0.31" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/ui/src/View/index.scss: -------------------------------------------------------------------------------- 1 | .bx--number { 2 | width: 100% 3 | } 4 | 5 | .coreSelect { 6 | width: 25em !important; 7 | height: 3.35em !important; 8 | max-height: 3.35em !important; 9 | background-color: #161616 !important; 10 | border: none !important; 11 | } 12 | 13 | .coreSelect .bx--list-box__selection { 14 | visibility: hidden; 15 | } 16 | .coreSelect .bx--text-input { 17 | border: none !important; 18 | } 19 | 20 | .comboBoxNoClear .bx--list-box__selection { 21 | visibility: hidden; 22 | } 23 | 24 | .clock .bx--tooltip__trigger.bx--tooltip__trigger--definition { 25 | font-size: 1rem; 26 | border-bottom: none; 27 | margin-bottom: 0.4em; 28 | } 29 | 30 | .dx--dashboard-logs .bx--data-table-content { 31 | overflow-x: clip !important; 32 | } 33 | 34 | .dx--table-empty { 35 | height: 88vh; 36 | } 37 | 38 | .dx--light { 39 | .clock .bx--tooltip__trigger.bx--tooltip__trigger--definition { 40 | color: #f4f4f4; 41 | } 42 | 43 | .coreSelect .bx--text-input { 44 | color: #f4f4f4; 45 | } 46 | 47 | .coreSelect.bx--list-box--disabled .bx--text-input { 48 | color: #525252; 49 | } 50 | 51 | .coreSelect .bx--list-box__menu-icon > svg { 52 | fill: #f4f4f4; 53 | } 54 | 55 | .coreSelect.bx--list-box--disabled .bx--list-box__menu-icon > svg { 56 | fill: #525252; 57 | } 58 | 59 | .bx--data-table { 60 | background: var(--cds-ui-03, #e0e0e0) 61 | } 62 | 63 | .dx--table-empty { 64 | background: #ffffff 65 | } 66 | } 67 | 68 | .dx--dark { 69 | .dx--table-empty { 70 | background: var(--cds-ui-01, #f4f4f4) 71 | } 72 | } 73 | 74 | .dx--content { 75 | max-width: 100% !important; 76 | margin-right: 2em !important; 77 | margin-left: 20em !important; 78 | } 79 | 80 | .bx--modal { 81 | margin-left: 18em !important; 82 | height: 100% !important; 83 | width: 86.2% !important; 84 | } 85 | 86 | .bx--modal.is-visible { 87 | width: 87.2% !important; 88 | } 89 | 90 | .bx--header__name { 91 | min-width: 18.3em; 92 | } 93 | 94 | @media only screen and (max-width: 1055px) { 95 | .dx--content { 96 | max-width: 100% !important; 97 | margin-right: -1em !important; 98 | margin-left: 2em !important; 99 | transition: margin-left 0.1s ease-in-out, margin-right 0.1s ease-in-out; 100 | } 101 | 102 | .bx--modal { 103 | margin-left: 3em !important; 104 | height: 100% !important; 105 | width: 96% !important; 106 | } 107 | 108 | .bx--header__name { 109 | min-width: 0; 110 | transition: min-width 0.1s ease-in-out; 111 | } 112 | 113 | .bx--header__nav { 114 | display: block !important; 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /packages/ui/src/View/Panels/queries.js: -------------------------------------------------------------------------------- 1 | const panelsGQL = `query panels($realm: String, $core: String) { 2 | stacks(core: $core, realm: $realm) { 3 | id 4 | label 5 | panelLabel 6 | description 7 | } 8 | panels(core: $core, realm: $realm) { 9 | id 10 | label 11 | description 12 | layout { 13 | id 14 | label 15 | rows 16 | columns 17 | } 18 | layoutType { 19 | id 20 | label 21 | } 22 | buttons { 23 | row 24 | column 25 | stack { 26 | id 27 | label 28 | panelLabel 29 | description 30 | } 31 | } 32 | } 33 | controllerLayouts { 34 | id 35 | label 36 | rows 37 | columns 38 | } 39 | }` 40 | 41 | const existingPanelGQL = `query panel($id: String, $realm: String, $core: String) { 42 | stacks(core: $core, realm: $realm) { 43 | id 44 | label 45 | panelLabel 46 | description 47 | tags { 48 | id 49 | label 50 | colour { 51 | id 52 | label 53 | } 54 | } 55 | colour { 56 | id 57 | label 58 | } 59 | } 60 | panel(id: $id) { 61 | id 62 | label 63 | description 64 | layout { 65 | id 66 | label 67 | rows 68 | columns 69 | } 70 | layoutType { 71 | id 72 | label 73 | } 74 | buttons { 75 | row 76 | column 77 | stack { 78 | id 79 | label 80 | panelLabel 81 | description 82 | tags { 83 | id 84 | label 85 | colour { 86 | id 87 | label 88 | } 89 | } 90 | colour { 91 | id 92 | label 93 | } 94 | } 95 | } 96 | } 97 | controllerLayouts { 98 | id 99 | label 100 | rows 101 | columns 102 | } 103 | }` 104 | 105 | const newPanelGQL = `query panels($realm: String, $core: String) { 106 | stacks(core: $core, realm: $realm) { 107 | id 108 | label 109 | panelLabel 110 | description 111 | tags { 112 | id 113 | label 114 | colour { 115 | id 116 | label 117 | } 118 | } 119 | colour { 120 | id 121 | label 122 | } 123 | } 124 | controllerLayouts { 125 | id 126 | label 127 | rows 128 | columns 129 | } 130 | }` 131 | 132 | const deletePanelGQL = ` 133 | mutation deletePanel($id: String) { 134 | deletePanel(id: $id) 135 | } 136 | ` 137 | 138 | const panelUpdateMutationGQL = ` 139 | mutation updatePanel($panel: panelUpdateInputType) { 140 | updatePanel(panel: $panel) { 141 | id 142 | } 143 | } 144 | ` 145 | 146 | export { panelsGQL, deletePanelGQL, panelUpdateMutationGQL, existingPanelGQL, newPanelGQL } 147 | -------------------------------------------------------------------------------- /packages/ui/src/View/Shotbox/ShotboxControllerWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Loading, ToastNotification } from 'carbon-components-react' 4 | import { useQuery, useSubscription } from 'urql' 5 | import { controllerSubscriptionGQL } from './queries' 6 | import ShotboxPanelWrapper from './ShotboxPanelWrapper.jsx' 7 | import GraphQLError from '../components/GraphQLError.jsx' 8 | 9 | const ShotboxControllerWrapper = ({ inline, match: { params: { id } } }) => { 10 | const [controller, setController] = useState({}) 11 | const [result, refresh] = useQuery({ 12 | query: `query controller($id: String) { 13 | controller(id: $id) { 14 | id 15 | label 16 | layout { 17 | id 18 | label 19 | rows 20 | columns 21 | } 22 | panel { 23 | id 24 | label 25 | } 26 | } 27 | }`, 28 | variables: { id: id }, 29 | pause: !id, 30 | requestPolicy: 'network-only' 31 | }) 32 | 33 | // eslint-disable-next-line no-unused-vars 34 | const [controllerUpdateSubscription] = useSubscription({ query: controllerSubscriptionGQL }, (messages = [], response) => { 35 | if (response.controller.id === controller.id) { 36 | console.log('subscription: ', response) 37 | refresh() 38 | setController({ ...response }) 39 | } 40 | return [response.newMessages, ...messages] 41 | }) 42 | 43 | if (result.error) { return } 44 | if (result.fetching && !controller.id) { return } 45 | if (result.data && !controller.id) { 46 | setController({ ...result.data.controller }) 47 | return (<>) 48 | } 49 | if (controller.panel?.id) { 50 | return ( 51 | <> 52 | { !inline && 53 | <> 54 | Controller: {controller.label} 55 |

56 | 57 | } 58 | 59 | 60 | ) 61 | } 62 | if (controller.id && !controller.panel?.id) { 63 | return ( 64 | 78 | ) 79 | } 80 | } 81 | 82 | ShotboxControllerWrapper.propTypes = { 83 | match: PropTypes.object, 84 | inline: PropTypes.bool 85 | } 86 | 87 | export default ShotboxControllerWrapper 88 | -------------------------------------------------------------------------------- /packages/link/src/network/graphql.js: -------------------------------------------------------------------------------- 1 | import { Client, defaultExchanges, subscriptionExchange } from 'urql' 2 | import { SubscriptionClient } from 'subscriptions-transport-ws' 3 | import { pipe, subscribe } from 'wonka' 4 | import { findIndex } from 'lodash' 5 | import { updateStreamdecks, streamDecks } from '../streamdeck' 6 | import { config } from '../utils/config' 7 | import WebSocket from 'ws' 8 | import log from '../utils/log' 9 | import { controllerUpdateSubscriptionGQL } from '../streamdeck/queries' 10 | 11 | let director 12 | let subscriptionClient 13 | 14 | const initGQLClient = (address, https) => { 15 | log('info', '/link/network/graphql', `Connecting to ${https ? 'https:' : 'http:'}//${address}/graphql`) 16 | subscriptionClient = new SubscriptionClient(`${https ? 'wss:' : 'ws:'}//${address}/graphql`, { reconnect: true, keepAlive: 5000 }, WebSocket) 17 | 18 | subscriptionClient.onDisconnected(() => { 19 | updateStreamdecks({ type: 'offline' }) 20 | config.set('connection', { ...config.get('connection'), status: false }) 21 | }) 22 | 23 | subscriptionClient.onReconnecting(() => { 24 | updateStreamdecks({ type: 'connecting' }) 25 | }) 26 | 27 | subscriptionClient.onReconnected(() => { 28 | setTimeout(() => { updateStreamdecks({ type: 'connected' }) }, 500) 29 | config.set('connection', { ...config.get('connection'), status: true }) 30 | }) 31 | 32 | subscriptionClient.onConnecting(() => { 33 | updateStreamdecks({ type: 'connecting' }) 34 | }) 35 | 36 | subscriptionClient.onConnected(() => { 37 | setTimeout(() => { updateStreamdecks({ type: 'connected' }) }, 500) 38 | config.set('connection', { ...config.get('connection'), status: true }) 39 | }) 40 | 41 | subscriptionClient.onError(error => log('error', '/link/network/graphql', error.message)) 42 | 43 | director = new Client({ 44 | url: `${https ? 'https:' : 'http:'}//${address}/graphql`, 45 | requestPolicy: 'network-only', 46 | maskTypename: true, 47 | exchanges: [ 48 | ...defaultExchanges, 49 | subscriptionExchange({ 50 | forwardSubscription (operation) { 51 | return subscriptionClient.request(operation) 52 | } 53 | }) 54 | ] 55 | }) 56 | 57 | pipe( 58 | director.subscription(controllerUpdateSubscriptionGQL), 59 | subscribe(result => { 60 | if (result.error) { 61 | log('error', 'link/network/graphql', result.error) 62 | } 63 | if (result.data && streamDecks.find(sd => sd.config.serial === result.data.controller.serial)) { 64 | streamDecks[findIndex(streamDecks, (streamdeck) => streamdeck.config.serial === result.data.controller.serial)].config = result.data.controller 65 | updateStreamdecks({ type: 'update', force: true, serial: result.data.controller.serial, panel: result.data.controller.panel.buttons ? result.data.controller.panel : null }) 66 | } 67 | }) 68 | ) 69 | } 70 | 71 | export { initGQLClient, director } 72 | -------------------------------------------------------------------------------- /packages/core/src/network/graphql/stackTypes/stackUpdateInputType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLString, 4 | GraphQLList, 5 | GraphQLInt 6 | } from 'graphql' 7 | import GraphQLJSON from 'graphql-type-json' 8 | import globalColourInputType from '../coreTypes/globalColourInputType' 9 | import tagInputType from '../tagTypes/tagInputType' 10 | 11 | const actionInputType = new GraphQLInputObjectType({ 12 | name: 'stackActionInputType', 13 | description: 'An action is something that happens on a device or piece of software', 14 | fields: { 15 | delay: { 16 | type: GraphQLInt 17 | }, 18 | device: { 19 | type: new GraphQLInputObjectType({ 20 | name: 'stackDeviceInputType', 21 | fields: { 22 | id: { 23 | type: GraphQLString 24 | }, 25 | label: { 26 | type: GraphQLString 27 | }, 28 | provider: { 29 | type: new GraphQLInputObjectType({ 30 | name: 'stackDeviceProviderInputType', 31 | fields: { 32 | id: { 33 | type: GraphQLString 34 | }, 35 | label: { 36 | type: GraphQLString 37 | } 38 | } 39 | }) 40 | } 41 | } 42 | }) 43 | }, 44 | providerFunction: { 45 | type: new GraphQLInputObjectType({ 46 | name: 'stackDeviceProviderFunctionInputType', 47 | fields: { 48 | id: { 49 | type: GraphQLString 50 | }, 51 | label: { 52 | type: GraphQLString 53 | } 54 | } 55 | }) 56 | }, 57 | parameters: { 58 | type: new GraphQLList( 59 | new GraphQLInputObjectType({ 60 | name: 'stackActionParametersInputType', 61 | fields: { 62 | id: { 63 | type: GraphQLString 64 | }, 65 | value: { 66 | type: GraphQLJSON 67 | } 68 | } 69 | }) 70 | ) 71 | } 72 | } 73 | }) 74 | 75 | const stackUpdateInputType = new GraphQLInputObjectType({ 76 | name: 'stackUpdateInputType', 77 | description: 'A Stack is a group of actions that can be triggered at once or sequentially by a controller', 78 | fields: { 79 | id: { 80 | type: GraphQLString 81 | }, 82 | label: { 83 | type: GraphQLString 84 | }, 85 | panelLabel: { 86 | type: GraphQLString 87 | }, 88 | description: { 89 | type: GraphQLString 90 | }, 91 | tags: { 92 | type: new GraphQLList(GraphQLString) 93 | }, 94 | realm: { 95 | type: GraphQLString 96 | }, 97 | core: { 98 | type: GraphQLString 99 | }, 100 | colour: { 101 | type: globalColourInputType 102 | }, 103 | actions: { 104 | type: new GraphQLList( 105 | actionInputType 106 | ) 107 | } 108 | } 109 | }) 110 | 111 | export default stackUpdateInputType 112 | export { actionInputType } -------------------------------------------------------------------------------- /packages/ui/src/View/Dashboard/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import globalContext from '../../globalContext' 3 | 4 | import Logs from './Logs.jsx' 5 | import Status from './Status.jsx' 6 | import ResourceSummary from './ResourceSummary.jsx' 7 | import Notes from './Notes.jsx' 8 | 9 | import { Grid, Row, Column, Tile, Link } from 'carbon-components-react' 10 | 11 | const Dashboard = () => { 12 | const { contextRealm } = useContext(globalContext) 13 | const tileHeight = '415px' 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

26 |
Resource summary
27 | 28 |
29 |
30 | 31 | 32 |
Quick Links

33 |
    34 |
  • 35 | New Device 36 |
  • 37 |
  • 38 | New Stack 39 |
  • 40 |
  • 41 | New Panel 42 |
  • 43 |
  • 44 | New Virtual Controller 45 |
  • 46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | {/* 55 | 56 |
About Director

57 |

58 | Director is a FOSS project from BorealSystems. 59 |

We are very greatful for everyones support during ongoing development, in particular we would like to thank our contributers, 60 | both Code/Design and Financial, 61 | as well as everyone on the Video Engineering Discord who provided much needed beta testing and motivation. 62 |

If you find any bugs or would like to request features/devices you can do so on our Phabricator. 63 |

Thank you for using Director. 64 |

65 |
66 |
*/} 67 |
68 |
69 | ) 70 | } 71 | 72 | export default Dashboard 73 | -------------------------------------------------------------------------------- /packages/ui/src/View/Tags/TagModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { useMutation } from 'urql' 4 | 5 | import { 6 | ComboBox, 7 | InlineNotification, 8 | InlineLoading, 9 | Modal, 10 | Tag, 11 | TextInput 12 | } from 'carbon-components-react' 13 | import { Tag16 } from '@carbon/icons-react' 14 | 15 | import { updateTagMutationGQL } from './queries' 16 | 17 | const TagModal = ({ tagData, render, updateTagsQuery, globalColours }) => { 18 | const [tag, setTag] = useState({ ...tagData }) 19 | const [tagModalSubmitting, setTagModalSubmitting] = useState(false) 20 | const [tagModalVisible, setTagModalVisible] = useState(false) 21 | const openTag = () => setTagModalVisible(true) 22 | 23 | const [tagMutationResult, tagUpdateMutation] = useMutation(updateTagMutationGQL) 24 | 25 | const closeTagModal = () => setTagModalVisibility(false) 26 | 27 | const submitAndCloseTag = () => { 28 | setTagModalSubmitting(true) 29 | tagUpdateMutation({ tag: tag }) 30 | .then((response) => { 31 | setTagModalSubmitting(false) 32 | if (!response.error) { 33 | updateTagsQuery() 34 | setTagModalVisible(false) 35 | } 36 | }) 37 | } 38 | 39 | return ( 40 | <> 41 | { render(openTag) } 42 | {tag.label}   {tag.label ?? 'New Tag'}} 46 | primaryButtonText={tagModalSubmitting ? <>Updating Tag : 'Update Tag'} 47 | primaryButtonDisabled={tagModalSubmitting || !tag.label || !tag.colour } 48 | secondaryButtonText='Cancel' 49 | open={tagModalVisible} 50 | onRequestClose={closeTagModal} 51 | onRequestSubmit={submitAndCloseTag} 52 | onSecondarySubmit={closeTagModal} 53 | > 54 | { setTag({ ...tag, colour: e.selectedItem}) }} 62 | />
63 |
70 | { setTag({ ...tag, label: e.target.value.slice(0, 100) }) }} 78 | /> 79 | { tagMutationResult.error && 80 | 85 | } 86 |
87 | 88 | ) 89 | } 90 | 91 | TagModal.propTypes = { 92 | realmData: PropTypes.object, 93 | render: PropTypes.func, 94 | updateRealmsQuery: PropTypes.func, 95 | globalColours: PropTypes.array 96 | } 97 | 98 | export default TagModal 99 | -------------------------------------------------------------------------------- /packages/ui/src/View/Stacks/ActionParameter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Column, ComboBox, Row, TextInput, NumberInput, MultiSelect } from 'carbon-components-react' 3 | 4 | const ActionParameter = ({parameter, getParameterValue, setParameter}) => ( 5 | <> 6 | 7 | { parameter.inputType === 'textInput' && 8 | 9 | { setParameter(e.target.value, parameter.id) }} 17 | /> 18 | 19 | } 20 | { parameter.inputType === 'textAreaInput' && 21 | 22 |