├── .github
└── workflows
│ └── build-test.yml
├── .travis.yml
├── CONTRIBUTING.md
├── Development
├── .dockerignore
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .prettierrc.js
├── Dockerfile
├── package.json
├── public
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.js
│ ├── assets
│ │ └── sea-lion.png
│ ├── components
│ │ ├── ActiveField.js
│ │ ├── AppLogo.js
│ │ ├── CardFormIterator.js
│ │ ├── CollapseButton.js
│ │ ├── ConnectionEditActions.js
│ │ ├── ConnectionEditToolbar.js
│ │ ├── ConnectionShowActions.js
│ │ ├── ConstraintField.js
│ │ ├── CustomNameField.js
│ │ ├── CustomNamesContext.js
│ │ ├── CustomNamesContextProvider.js
│ │ ├── DeleteButton.js
│ │ ├── FilterPanel.js
│ │ ├── HintTypography.js
│ │ ├── ItemArrayField.js
│ │ ├── LinkChipField.js
│ │ ├── ListActions.js
│ │ ├── MappingButton.js
│ │ ├── MappingShowActions.js
│ │ ├── ObjectField.js
│ │ ├── ObjectInput.js
│ │ ├── PaginationButtons.js
│ │ ├── ParameterRegisters.js
│ │ ├── RateField.js
│ │ ├── RawButton.js
│ │ ├── ResourceShowActions.js
│ │ ├── ResourceTitle.js
│ │ ├── SanitizedDivider.js
│ │ ├── TAIField.js
│ │ ├── TransportFileViewer.js
│ │ ├── URLField.js
│ │ ├── UnsortableDatagrid.js
│ │ ├── labelize.js
│ │ ├── makeConnection.js
│ │ ├── sanitizeRestProps.js
│ │ ├── useCustomNamesContext.js
│ │ ├── useDebounce.js
│ │ └── useGetList.js
│ ├── config.json
│ ├── dataProvider.js
│ ├── icons
│ │ ├── ActivateImmediate.js
│ │ ├── ActivateScheduled.js
│ │ ├── CancelScheduledActivation.js
│ │ ├── ConnectRegistry.js
│ │ ├── ContentCopy.js
│ │ ├── Device.js
│ │ ├── Flow.js
│ │ ├── JsonIcon.js
│ │ ├── Node.js
│ │ ├── Receiver.js
│ │ ├── Registry.js
│ │ ├── RegistryLogs.js
│ │ ├── Sender.js
│ │ ├── Source.js
│ │ ├── Stage.js
│ │ ├── Subscription.js
│ │ ├── index.js
│ │ └── svgr-template.js
│ ├── index.css
│ ├── index.js
│ ├── pages
│ │ ├── about.js
│ │ ├── appbar.js
│ │ ├── devices
│ │ │ ├── ChannelMappingMatrix.js
│ │ │ ├── DevicesList.js
│ │ │ ├── DevicesShow.js
│ │ │ ├── FilterMatrix.js
│ │ │ └── index.js
│ │ ├── flows
│ │ │ ├── FlowsList.js
│ │ │ ├── FlowsShow.js
│ │ │ └── index.js
│ │ ├── logs
│ │ │ ├── LogsList.js
│ │ │ ├── LogsShow.js
│ │ │ └── index.js
│ │ ├── menu.js
│ │ ├── nodes
│ │ │ ├── NodesList.js
│ │ │ ├── NodesShow.js
│ │ │ └── index.js
│ │ ├── queryapis
│ │ │ ├── ConnectButton.js
│ │ │ ├── QueryAPIsList.js
│ │ │ ├── QueryAPIsShow.js
│ │ │ └── index.js
│ │ ├── receivers
│ │ │ ├── ConnectButtons.js
│ │ │ ├── ConnectionManagementTab.js
│ │ │ ├── ReceiverConstraintSets.js
│ │ │ ├── ReceiverTransportParams.js
│ │ │ ├── ReceiversEdit.js
│ │ │ ├── ReceiversList.js
│ │ │ ├── ReceiversShow.js
│ │ │ └── index.js
│ │ ├── senders
│ │ │ ├── SenderTransportParams.js
│ │ │ ├── SendersEdit.js
│ │ │ ├── SendersList.js
│ │ │ ├── SendersShow.js
│ │ │ └── index.js
│ │ ├── settings.js
│ │ ├── sources
│ │ │ ├── SourcesList.js
│ │ │ ├── SourcesShow.js
│ │ │ └── index.js
│ │ └── subscriptions
│ │ │ ├── SubscriptionsCreate.js
│ │ │ ├── SubscriptionsList.js
│ │ │ ├── SubscriptionsShow.js
│ │ │ └── index.js
│ ├── registerServiceWorker.js
│ ├── settings.js
│ └── theme
│ │ └── ThemeContext.js
└── yarn.lock
├── Documents
├── Dependencies.md
├── Getting-Started.md
├── Repository-Structure.md
└── images
│ ├── jt-nm-tested-03-20-controller.png
│ └── sea-lion.png
├── LICENSE
├── NOTICE
└── README.md
/.github/workflows/build-test.yml:
--------------------------------------------------------------------------------
1 | name: 'build-test'
2 |
3 | on: [pull_request, push]
4 |
5 | defaults:
6 | run:
7 | working-directory: Development
8 |
9 | jobs:
10 | build_and_test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - uses: actions/setup-node@v1
16 |
17 | - run: yarn install
18 |
19 | - run: yarn run lint-check
20 | - run: yarn run build
21 | - run: yarn test --passWithNoTests --coverage --watchAll=false
22 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: lts/*
3 |
4 | before_install: cd Development
5 |
6 | install: yarn install
7 |
8 | script:
9 | - yarn run lint-check
10 | - yarn run build
11 | - yarn test --passWithNoTests --coverage --watchAll=false
12 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for your interest in this project! We want to encourage individuals and organisations to be involved in its ongoing development as users and contributors.
4 |
5 | ## Issues
6 |
7 | We welcome bug reports and feature requests for the implementation. These should be submitted as Issues on GitHub:
8 |
9 | - [Sign up](https://github.com/join) for GitHub if you haven't already done so.
10 | - Check whether someone has already submitted a similar Issue.
11 | - If necessary, submit a new Issue.
12 |
13 | Good bug reports will include a clear title and description, as much relevant information as possible, and preferably a code sample or an executable test case demonstrating the expected behavior that is not occurring.
14 |
15 | ## Pull Requests
16 |
17 | You can submit bug fixes, patches and other improvements to the code or documentation for review through a GitHub pull request as follows:
18 |
19 | * Fork the repository.
20 | * Make and commit changes.
21 | * Push your changes to a topic branch in your fork.
22 | * Make sure you understand how the repository licence applies to pull requests (see below).
23 | * Submit a pull request.
24 |
25 | Good PRs will include a clear rationale for the patch, and should follow the style of the existing code or documentation in this area.
26 |
27 | ## Licence Agreement
28 |
29 | To make the process of contributing as simple as possible, we do not require contributors to have signed a Contributor Licence Agreement (CLA).
30 |
31 | As GitHub says in its [Terms of Service](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license), *"Whenever you make a contribution to a repository containing notice of a license, you license your contribution under the same terms, and you agree that you have the right to license your contribution under those terms. [...] This is widely accepted as the norm in the open-source community; it's commonly referred to by the shorthand "inbound=outbound"."*
32 |
33 | All submissions are therefore made under the terms and conditions of the widely-used Apache License 2.0 with which this repository is [licensed](LICENSE). In brief, it explains that you must be authorised to submit on behalf of the copyright owner and that you simply grant the project's users, maintainers and contributors the same ability to use your submission as the rest of the repository.
34 |
35 | ## Joining the AMWA Networked Media Incubator
36 |
37 | If you wish to get more involved in creating NMOS specifications, working to implement and improve them, and participating in "plugfests", then your organisation may benefit from joining the [Advanced Media Workflow Association (AMWA)](http://amwa.tv/).
38 |
39 |
--------------------------------------------------------------------------------
/Development/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 |
--------------------------------------------------------------------------------
/Development/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | lib
4 | esm
5 |
--------------------------------------------------------------------------------
/Development/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "react-app",
5 | "plugin:prettier/recommended",
6 | "prettier/@typescript-eslint",
7 | "prettier/babel",
8 | "prettier/react"
9 | ],
10 | "plugins": [
11 | "@typescript-eslint",
12 | "import",
13 | "jsx-a11y",
14 | "prettier",
15 | "react",
16 | "react-hooks"
17 | ],
18 | "parserOptions": {
19 | "ecmaVersion": 6,
20 | "ecmaFeatures": {
21 | "jsx": true
22 | }
23 | },
24 | "rules": {
25 | "sort-imports": ["error", {
26 | "ignoreCase": false,
27 | "ignoreDeclarationSort": true,
28 | "ignoreMemberSort": false,
29 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
30 | }],
31 | "no-var": "warn",
32 | "eol-last": ["error", "always"],
33 | "no-use-before-define": "off",
34 | "prettier/prettier": "error"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Development/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 | /.eslintcache
23 |
--------------------------------------------------------------------------------
/Development/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSpacing: true,
4 | endOfLine: 'auto',
5 | jsxBracketSameLine: false,
6 | jsxSingleQuote: false,
7 | printWidth: 80,
8 | quoteProps: 'as-needed',
9 | rangeStart: 0,
10 | rangeEnd: Infinity,
11 | semi: true,
12 | singleQuote: true,
13 | tabWidth: 4,
14 | trailingComma: 'es5',
15 | useTabs: false,
16 | };
17 |
--------------------------------------------------------------------------------
/Development/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:latest AS build
2 | WORKDIR /usr/src/app
3 | COPY package.json yarn.lock ./
4 | RUN yarn
5 | COPY . ./
6 | RUN yarn build
7 |
8 | FROM nginx:alpine
9 | COPY --from=build /usr/src/app/build /usr/share/nginx/html
10 | EXPOSE 80
11 | ENTRYPOINT ["nginx", "-g", "daemon off;"]
12 |
--------------------------------------------------------------------------------
/Development/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nmos-js",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": ".",
6 | "dependencies": {
7 | "@material-ui/core": "^4.3.3",
8 | "@material-ui/icons": "^4.2.1",
9 | "@material-ui/lab": "^4.0.0-alpha.57",
10 | "@material-ui/styles": "^4.3.3",
11 | "clipboard-copy": "^4.0.1",
12 | "dayjs": "^1.8.23",
13 | "deep-diff": "^1.0.2",
14 | "final-form": "^4.18.5",
15 | "json-ptr": "^2.0.0",
16 | "lodash": "~4.17.5",
17 | "prop-types": "^15.6.2",
18 | "react": "^17.0.1",
19 | "react-admin": "^3.11.3",
20 | "react-dom": "^17.0.1",
21 | "react-final-form": "^6.3.3",
22 | "react-redux": "^7.1.0",
23 | "react-router-dom": "^5.1.0",
24 | "react-scripts": "^4.0.1",
25 | "redux": "^3.7.2 || ^4.0.3",
26 | "t-a-i": "^1.0.15"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test --env=jsdom",
32 | "eject": "react-scripts eject",
33 | "lint": "eslint --fix ./ && prettier --config .prettierrc.js --write src/**/*.js",
34 | "lint-check": "eslint --fix-dry-run ./ && prettier --config .prettierrc.js --check src/**/*.js"
35 | },
36 | "devDependencies": {
37 | "@typescript-eslint/eslint-plugin": "^4.9.1",
38 | "@typescript-eslint/parser": "^4.9.1",
39 | "babel-eslint": "^10.0.3",
40 | "eslint": "^7.15.0",
41 | "eslint-config-prettier": "^7.0.0",
42 | "eslint-config-react-app": "^6.0.0",
43 | "eslint-plugin-flowtype": "^5.2.0",
44 | "eslint-plugin-import": "^2.18.2",
45 | "eslint-plugin-jsx-a11y": "^6.2.3",
46 | "eslint-plugin-prettier": "^3.1.1",
47 | "eslint-plugin-react": "^7.16.0",
48 | "eslint-plugin-react-hooks": "^4.2.0",
49 | "prettier": "^2.2.1",
50 | "typescript": "^4.1.2"
51 | },
52 | "browserslist": {
53 | "production": [
54 | ">0.2%",
55 | "not dead",
56 | "not op_mini all"
57 | ],
58 | "development": [
59 | "last 1 chrome version",
60 | "last 1 firefox version",
61 | "last 1 safari version"
62 | ]
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Development/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
21 |
22 |
23 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Development/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "nmos-js",
3 | "start_url": "./index.html",
4 | "display": "standalone",
5 | "theme_color": "rgb(0,47,103)",
6 | "background_color": "#ffffff"
7 | }
8 |
--------------------------------------------------------------------------------
/Development/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Admin, Layout, Resource } from 'react-admin';
3 | import { useTheme } from '@material-ui/styles';
4 | import { get } from 'lodash';
5 | import CONFIG from './config.json';
6 | import { SettingsContextProvider } from './settings';
7 | import AdminMenu from './pages/menu';
8 | import AppBar from './pages/appbar';
9 | import About from './pages/about';
10 | import { NodesList, NodesShow } from './pages/nodes';
11 | import { DevicesList, DevicesShow } from './pages/devices';
12 | import { SourcesList, SourcesShow } from './pages/sources';
13 | import { FlowsList, FlowsShow } from './pages/flows';
14 | import { ReceiversEdit, ReceiversList, ReceiversShow } from './pages/receivers';
15 | import { SendersEdit, SendersList, SendersShow } from './pages/senders';
16 | import { LogsList, LogsShow } from './pages/logs';
17 | import {
18 | SubscriptionsCreate,
19 | SubscriptionsList,
20 | SubscriptionsShow,
21 | } from './pages/subscriptions';
22 | import { QueryAPIsList, QueryAPIsShow } from './pages/queryapis';
23 | import Settings from './pages/settings';
24 | import dataProvider from './dataProvider';
25 |
26 | const AdminAppBar = props => ;
27 |
28 | const AdminLayout = props => (
29 |
30 | );
31 |
32 | const AppAdmin = () => (
33 |
40 |
41 |
42 |
43 |
44 |
45 |
51 |
57 |
63 |
64 |
70 |
71 | );
72 |
73 | export const App = () => (
74 |
75 |
76 |
77 | );
78 |
79 | export default App;
80 |
--------------------------------------------------------------------------------
/Development/src/assets/sea-lion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sony/nmos-js/5896ef98dfa236eb76c2521f33b699a14450d85d/Development/src/assets/sea-lion.png
--------------------------------------------------------------------------------
/Development/src/components/ActiveField.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Switch } from '@material-ui/core';
3 | import { useNotify } from 'react-admin';
4 | import get from 'lodash/get';
5 | import dataProvider from '../dataProvider';
6 | import sanitizeRestProps from './sanitizeRestProps';
7 |
8 | const toggleMasterEnable = (record, resource) => {
9 | return new Promise((resolve, reject) =>
10 | dataProvider('GET_ONE', resource, {
11 | id: record.id,
12 | })
13 | .then(({ data }) => {
14 | if (!data.hasOwnProperty('$staged')) {
15 | throw new Error('No Connection API found');
16 | }
17 | const params = {
18 | id: get(data, 'id'),
19 | data: {
20 | ...data,
21 | $staged: {
22 | ...get(data, '$staged'),
23 | master_enable: !get(data, '$active.master_enable'),
24 | activation: { mode: 'activate_immediate' },
25 | },
26 | },
27 | previousData: data,
28 | };
29 | return dataProvider('UPDATE', resource, params);
30 | })
31 | .then(response => resolve(response))
32 | .catch(error => reject(error))
33 | );
34 | };
35 |
36 | const ActiveField = ({ className, source, record = {}, resource, ...rest }) => {
37 | const notify = useNotify();
38 | const [checked, setChecked] = React.useState(
39 | get(record, 'subscription.active')
40 | );
41 |
42 | const handleChange = (record, resource) => {
43 | toggleMasterEnable(record, resource)
44 | .then(({ data }) => setChecked(get(data, 'master_enable')))
45 | .catch(error => notify(error.toString(), 'warning'));
46 | };
47 |
48 | // When the page refresh button is pressed, the ActiveField will receive a
49 | // new record prop. When this happens we should update the state of the
50 | // switch to reflect the newest IS-04 data
51 | useEffect(() => {
52 | setChecked(get(record, 'subscription.active'));
53 | }, [record]);
54 |
55 | return (
56 | handleChange(record, resource)}
60 | className={className}
61 | value={checked}
62 | {...sanitizeRestProps(rest)}
63 | />
64 | );
65 | };
66 |
67 | ActiveField.defaultProps = {
68 | addLabel: true,
69 | };
70 |
71 | export default ActiveField;
72 |
--------------------------------------------------------------------------------
/Development/src/components/AppLogo.js:
--------------------------------------------------------------------------------
1 | import AppLogoAsset from '../assets/sea-lion.png';
2 |
3 | const AppLogoStyle = {
4 | border: '1px solid lightgray',
5 | borderRadius: '50%',
6 | padding: '4px',
7 | margin: '16px',
8 | maxWidth: '50%',
9 | };
10 |
11 | export const AppLogo = ({
12 | src = AppLogoAsset,
13 | style = { ...AppLogoStyle },
14 | ...props
15 | }) =>
;
16 |
17 | export default AppLogo;
18 |
--------------------------------------------------------------------------------
/Development/src/components/CardFormIterator.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Children,
3 | Component,
4 | cloneElement,
5 | isValidElement,
6 | } from 'react';
7 | import PropTypes from 'prop-types';
8 | import get from 'lodash/get';
9 | import { Card, CardContent, Grid } from '@material-ui/core';
10 | import { FormInput } from 'react-admin';
11 | // Derived from react-admin component
12 | export class CardFormIterator extends Component {
13 | constructor(props) {
14 | super(props);
15 | // we need a unique id for each field for a proper enter/exit animation
16 | // but redux-form doesn't provide one (cf https://github.com/erikras/redux-form/issues/2735)
17 | // so we keep an internal map between the field position and an autoincrement id
18 | this.nextId = props.fields.length
19 | ? props.fields.length
20 | : props.defaultValue
21 | ? props.defaultValue.length
22 | : 0;
23 |
24 | // We check whether we have a defaultValue (which must be an array) before checking
25 | // the fields prop which will always be empty for a new record.
26 | // Without it, our ids wouldn't match the default value and we would get key warnings
27 | // on the CssTransition element inside our render method
28 | this.ids = this.nextId > 0 ? Array.from(Array(this.nextId).keys()) : [];
29 | }
30 |
31 | render() {
32 | const {
33 | basePath,
34 | children,
35 | fields,
36 | record,
37 | resource,
38 | source,
39 | } = this.props;
40 | const records = get(record, source);
41 | return fields ? (
42 | <>
43 |
44 |
45 | {fields.map((member, index) => (
46 |
47 |
48 |
49 | {Children.map(children, (input, index2) =>
50 | isValidElement(input) ? (
51 |
77 | ) : null
78 | )}
79 |
80 |
81 |
82 | ))}
83 |
84 | >
85 | ) : null;
86 | }
87 | }
88 |
89 | CardFormIterator.propTypes = {
90 | defaultValue: PropTypes.any,
91 | basePath: PropTypes.string,
92 | children: PropTypes.node,
93 | fields: PropTypes.object,
94 | record: PropTypes.object,
95 | source: PropTypes.string,
96 | resource: PropTypes.string,
97 | };
98 |
99 | export default CardFormIterator;
100 |
--------------------------------------------------------------------------------
/Development/src/components/CollapseButton.js:
--------------------------------------------------------------------------------
1 | import { IconButton } from '@material-ui/core';
2 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
3 | import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft';
4 | import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight';
5 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
6 |
7 | const CollapseButton = ({
8 | onClick,
9 | isExpanded,
10 | direction = 'vertical',
11 | title,
12 | }) => (
13 |
14 | {direction === 'horizontal' ? (
15 | isExpanded ? (
16 |
17 | ) : (
18 |
19 | )
20 | ) : isExpanded ? (
21 |
22 | ) : (
23 |
24 | )}
25 |
26 | );
27 |
28 | export default CollapseButton;
29 |
--------------------------------------------------------------------------------
/Development/src/components/ConnectionEditActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { ShowButton, TopToolbar } from 'react-admin';
4 | import { useTheme } from '@material-ui/styles';
5 |
6 | export default function ConnectionEditActions({ basePath, id }) {
7 | const theme = useTheme();
8 | return (
9 |
20 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/Development/src/components/ConnectionEditToolbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useFormState } from 'react-final-form';
3 | import { Button } from '@material-ui/core';
4 | import { Toolbar } from 'react-admin';
5 | import get from 'lodash/get';
6 | import {
7 | ActivateImmediateIcon,
8 | ActivateScheduledIcon,
9 | CancelScheduledActivationIcon,
10 | StageIcon,
11 | } from '../icons';
12 |
13 | const ConnectionEditToolbar = ({ handleSubmitWithRedirect }) => {
14 | const formState = useFormState().values;
15 | const buttonProps = (() => {
16 | if (get(formState, '$staged.activation.activation_time')) {
17 | return [
18 | 'Cancel Scheduled Activation',
19 | ,
20 | ];
21 | }
22 | switch (get(formState, '$staged.activation.mode')) {
23 | case 'activate_immediate':
24 | return ['Activate', ];
25 | case 'activate_scheduled_relative':
26 | case 'activate_scheduled_absolute':
27 | return ['Activate Scheduled', ];
28 | default:
29 | return ['Stage', ];
30 | }
31 | })();
32 | return (
33 |
34 | <>
35 |
47 | >
48 |
49 | );
50 | };
51 |
52 | export default ConnectionEditToolbar;
53 |
--------------------------------------------------------------------------------
/Development/src/components/ConnectionShowActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import { Button, ListButton, TopToolbar, useRecordContext } from 'react-admin';
4 | import get from 'lodash/get';
5 | import EditIcon from '@material-ui/icons/Edit';
6 | import JsonIcon from '../icons/JsonIcon';
7 | import { useTheme } from '@material-ui/styles';
8 | import { concatUrl } from '../settings';
9 | import { resourceUrl } from '../dataProvider';
10 |
11 | // cf. ResourceShowActions
12 | export default function ConnectionShowActions({ basePath, id, resource }) {
13 | const { record } = useRecordContext();
14 |
15 | let json_href;
16 | if (record) {
17 | const tab = window.location.href.split('/').pop();
18 | if (tab === 'active' || tab === 'staged' || tab === 'transportfile') {
19 | json_href = concatUrl(record.$connectionAPI, `/${tab}`);
20 | } else if (tab === 'connect') {
21 | } else {
22 | json_href = resourceUrl(resource, `/${id}`);
23 | }
24 | }
25 | const theme = useTheme();
26 | return (
27 |
38 | {json_href ? (
39 |
47 | ) : null}
48 |
53 | {get(record, '$connectionAPI') != null ? (
54 |
61 | ) : null}
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/Development/src/components/ConstraintField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import get from 'lodash/get';
3 | import { Typography } from '@material-ui/core';
4 |
5 | const renderConstraintValue = value => {
6 | return value.numerator
7 | ? `${value.numerator} : ${value.denominator ? value.denominator : 1}`
8 | : value;
9 | };
10 |
11 | const ConstraintField = ({ record, source }) => {
12 | const constraint = get(record, source);
13 | const minConstraint = get(constraint, 'minimum');
14 | const maxConstraint = get(constraint, 'maximum');
15 | const enumConstraint = get(constraint, 'enum');
16 | return (
17 | <>
18 | {(minConstraint != null || maxConstraint != null) && (
19 |
20 | {minConstraint != null && (
21 |
22 | {renderConstraintValue(minConstraint)}
23 |
24 | )}
25 | –
26 | {maxConstraint != null && (
27 |
28 | {renderConstraintValue(maxConstraint)}
29 |
30 | )}
31 |
32 | )}
33 | {enumConstraint != null && (
34 |
35 | {enumConstraint
36 | .map(item => renderConstraintValue(item))
37 | .join(', ')}
38 |
39 | )}
40 | >
41 | );
42 | };
43 |
44 | ConstraintField.defaultProps = {
45 | addLabel: true,
46 | };
47 |
48 | export default ConstraintField;
49 |
--------------------------------------------------------------------------------
/Development/src/components/CustomNameField.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import { IconButton, TextField, Typography } from '@material-ui/core';
3 | import CreateIcon from '@material-ui/icons/Create';
4 | import DeleteIcon from '@material-ui/icons/Delete';
5 | import ClearIcon from '@material-ui/icons/Clear';
6 | import DoneIcon from '@material-ui/icons/Done';
7 | import useCustomNamesContext from './useCustomNamesContext';
8 |
9 | export const CustomNameField = ({
10 | defaultValue,
11 | source,
12 | label,
13 | autoFocus,
14 | onEditStarted,
15 | onEditStopped,
16 | ...props
17 | }) => {
18 | const {
19 | getCustomName,
20 | setCustomName,
21 | unsetCustomName,
22 | } = useCustomNamesContext();
23 | const [editing, setEditing] = useState(false);
24 | const [value, setValue] = useState(
25 | getCustomName(source) || defaultValue || ''
26 | );
27 |
28 | const inputRef = useRef();
29 | useEffect(() => {
30 | const timeout = setTimeout(() => {
31 | if (autoFocus && editing) inputRef.current.focus();
32 | }, 100);
33 | return () => clearTimeout(timeout);
34 | }, [autoFocus, editing]);
35 |
36 | const handleEdit = () => {
37 | setEditing(true);
38 | onEditStarted();
39 | };
40 |
41 | const removeCustomName = () => {
42 | setValue(defaultValue);
43 | unsetCustomName(source);
44 | setEditing(false);
45 | onEditStopped();
46 | };
47 |
48 | const saveCustomName = () => {
49 | if (value !== defaultValue) {
50 | setCustomName(source, value);
51 | } else {
52 | unsetCustomName(source);
53 | }
54 | setEditing(false);
55 | onEditStopped();
56 | };
57 |
58 | const cancelCustomName = () => {
59 | setValue(getCustomName(source) || defaultValue || '');
60 | setEditing(false);
61 | onEditStopped();
62 | };
63 |
64 | return editing ? (
65 |
66 | setValue(event.target.value)}
72 | onFocus={event => event.target.select()}
73 | inputRef={inputRef}
74 | fullWidth={true}
75 | onKeyPress={event => {
76 | if (event.key === 'Enter') {
77 | saveCustomName();
78 | }
79 | }}
80 | {...props}
81 | />
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | ) : (
90 |
91 |
92 | {value}
93 |
94 |
95 |
96 |
97 | {getCustomName(source) && (
98 |
99 |
100 |
101 | )}
102 |
103 | );
104 | };
105 |
106 | export default CustomNameField;
107 |
--------------------------------------------------------------------------------
/Development/src/components/CustomNamesContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export const CustomNamesContext = createContext();
4 | export default CustomNamesContext;
5 |
--------------------------------------------------------------------------------
/Development/src/components/CustomNamesContextProvider.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CustomNamesContext from './CustomNamesContext';
3 |
4 | export const CustomNamesContextProvider = props => (
5 |
6 | );
7 | export default CustomNamesContextProvider;
8 |
--------------------------------------------------------------------------------
/Development/src/components/DeleteButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import { Button, useDelete, useNotify, useRefresh } from 'react-admin';
4 | import get from 'lodash/get';
5 | import { makeStyles } from '@material-ui/core';
6 | import { fade } from '@material-ui/core/styles/colorManipulator';
7 | import DeleteIcon from '@material-ui/icons/Delete';
8 |
9 | const useStyles = makeStyles(theme => ({
10 | contained: {
11 | color: theme.palette.error.contrastText,
12 | backgroundColor: theme.palette.error.main,
13 | '&:hover': {
14 | backgroundColor: theme.palette.error.dark,
15 | // Reset on touch devices, it doesn't add specificity
16 | '@media (hover: none)': {
17 | backgroundColor: theme.palette.error.main,
18 | },
19 | },
20 | },
21 | text: {
22 | color: theme.palette.error.main,
23 | '&:hover': {
24 | backgroundColor: fade(
25 | theme.palette.error.main,
26 | theme.palette.action.hoverOpacity
27 | ),
28 | // Reset on touch devices, it doesn't add specificity
29 | '@media (hover: none)': {
30 | backgroundColor: 'transparent',
31 | },
32 | },
33 | },
34 | }));
35 |
36 | const DeleteButton = ({
37 | resource,
38 | id,
39 | record,
40 | variant = 'contained',
41 | size,
42 | }) => {
43 | const classes = useStyles();
44 | const notify = useNotify();
45 | const refresh = useRefresh();
46 | const history = useHistory();
47 | const [deleteOne, { loading }] = useDelete(resource, id, record, {
48 | onSuccess: () => {
49 | notify('Element deleted', 'info');
50 | if (window.location.hash.substr(1) === `/${resource}`) {
51 | refresh();
52 | } else {
53 | history.push(`/${resource}`);
54 | }
55 | },
56 | onFailure: error => {
57 | if (error.hasOwnProperty('body')) {
58 | notify(
59 | get(error.body, 'error') +
60 | ' - ' +
61 | get(error.body, 'code') +
62 | ' - ' +
63 | get(error.body, 'debug'),
64 | 'warning'
65 | );
66 | }
67 | notify(error.toString(), 'warning');
68 | },
69 | });
70 | return (
71 |
83 | );
84 | };
85 |
86 | export default DeleteButton;
87 |
--------------------------------------------------------------------------------
/Development/src/components/HintTypography.js:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react';
2 | import { Typography, withStyles } from '@material-ui/core';
3 |
4 | export const InlineTypography = forwardRef(({ style, ...props }, ref) => (
5 |
6 | ));
7 |
8 | export const hintStyle = theme => ({
9 | textDecorationLine: 'underline',
10 | textDecorationStyle: 'dotted',
11 | textDecorationColor: theme.palette.type === 'dark' ? '#696969' : '#c8c8c8',
12 | });
13 |
14 | const HintTypography = withStyles(theme => ({
15 | root: hintStyle(theme),
16 | }))(InlineTypography);
17 |
18 | export default HintTypography;
19 |
--------------------------------------------------------------------------------
/Development/src/components/ItemArrayField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography } from '@material-ui/core';
3 | import get from 'lodash/get';
4 |
5 | const ItemArrayField = ({ className, record, source }) => (
6 | <>
7 | {get(record, source, []).map((item, index) => (
8 |
9 | {item}
10 |
11 | ))}
12 | >
13 | );
14 | ItemArrayField.defaultProps = {
15 | addLabel: true,
16 | };
17 |
18 | export default ItemArrayField;
19 |
--------------------------------------------------------------------------------
/Development/src/components/LinkChipField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ChipField } from 'react-admin';
3 | import get from 'lodash/get';
4 |
5 | // most NMOS resources that we need to link to have a label property
6 | // which makes a sensible default source, but if it's empty, fall back
7 | // to the id property
8 | const LinkChipField = ({
9 | record,
10 | source = 'label',
11 | transform = _ => _,
12 | ...props
13 | }) => (
14 |
22 | );
23 |
24 | export default LinkChipField;
25 |
--------------------------------------------------------------------------------
/Development/src/components/ListActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, CreateButton, TopToolbar } from 'react-admin';
3 | import { useTheme } from '@material-ui/styles';
4 | import JsonIcon from '../icons/JsonIcon';
5 |
6 | const ListActions = ({ basePath, hasCreate, url }) => {
7 | const theme = useTheme();
8 | return (
9 |
20 | {url && (
21 |
30 | )}
31 | {hasCreate && }
32 |
33 | );
34 | };
35 |
36 | export default ListActions;
37 |
--------------------------------------------------------------------------------
/Development/src/components/MappingButton.js:
--------------------------------------------------------------------------------
1 | import { IconButton, withStyles } from '@material-ui/core';
2 | import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline';
3 | import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUnchecked';
4 |
5 | // de-emphasize the unchecked state
6 | const faded = { opacity: 0.3 };
7 |
8 | const styles = {
9 | unchecked: faded,
10 | checked: {},
11 | };
12 |
13 | // filter out our classes to avoid the Material-UI console warning
14 | const MappingButton = ({
15 | checked,
16 | classes: {
17 | checked: checkedClass,
18 | unchecked: uncheckedClass,
19 | ...inheritedClasses
20 | },
21 | ...props
22 | }) => (
23 |
28 | {checked ? : }
29 |
30 | );
31 |
32 | export default withStyles(styles)(MappingButton);
33 |
--------------------------------------------------------------------------------
/Development/src/components/MappingShowActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, ListButton, TopToolbar, useRecordContext } from 'react-admin';
3 | import JsonIcon from '../icons/JsonIcon';
4 | import { useTheme } from '@material-ui/styles';
5 | import { concatUrl } from '../settings';
6 | import { resourceUrl } from '../dataProvider';
7 |
8 | // cf. ResourceShowActions
9 | export default function MappingShowActions({ basePath, id, resource }) {
10 | const { record } = useRecordContext();
11 | let json_href;
12 | const theme = useTheme();
13 | if (record) {
14 | const tab = window.location.href.split('/').pop();
15 | if (tab === 'active_map' && record.$channelmappingAPI) {
16 | json_href = concatUrl(record.$channelmappingAPI, '/map/active');
17 | } else {
18 | json_href = resourceUrl(resource, `/${id}`);
19 | }
20 | }
21 | return (
22 |
33 | {json_href ? (
34 |
42 | ) : null}
43 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/Development/src/components/ObjectField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Table,
4 | TableBody,
5 | TableCell,
6 | TableHead,
7 | TableRow,
8 | } from '@material-ui/core';
9 | import { get, isEmpty, map } from 'lodash';
10 | import { Parameter } from './ParameterRegisters';
11 |
12 | export const ObjectField = ({ register, record, source }) =>
13 | // no table at all for empty objects
14 | !isEmpty(get(record, source)) && (
15 |
16 |
17 |
18 | Name
19 | Value(s)
20 |
21 |
22 |
23 | {map(get(record, source), (value, key) => (
24 |
25 |
26 |
27 |
28 | {Array.isArray(value) ? (
29 | {value.join(', ')}
30 | ) : (
31 | {value}
32 | )}
33 |
34 | ))}
35 |
36 |
37 | );
38 |
39 | ObjectField.defaultProps = {
40 | addLabel: true,
41 | };
42 |
43 | export default ObjectField;
44 |
--------------------------------------------------------------------------------
/Development/src/components/ObjectInput.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { FieldTitle, isRequired } from 'react-admin';
3 | import { useField, useForm } from 'react-final-form';
4 | import {
5 | FilledInput,
6 | IconButton,
7 | InputLabel,
8 | Table,
9 | TableBody,
10 | TableCell,
11 | TableFooter,
12 | TableHead,
13 | TableRow,
14 | withStyles,
15 | } from '@material-ui/core';
16 | import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
17 | import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline';
18 |
19 | const TableInput = withStyles(() => {
20 | return {
21 | input: {
22 | padding: '6px 10px',
23 | },
24 | };
25 | })(FilledInput);
26 |
27 | const ObjectInput = ({
28 | className,
29 | label,
30 | record,
31 | resource,
32 | source,
33 | validate,
34 | variant,
35 | margin = 'dense',
36 | ...rest
37 | }) => {
38 | const fieldProps = useField(source, {
39 | initialValue: undefined,
40 | ...rest,
41 | });
42 | const formProps = useForm();
43 | const { change } = formProps;
44 | const [data, setData] = useState(() => {
45 | let initialData = [];
46 | for (const key of Object.keys(fieldProps.input.value)) {
47 | initialData.push([key, fieldProps.input.value[key]]);
48 | }
49 | return initialData;
50 | });
51 | const keys = data.map(keyValuePair => keyValuePair[0]);
52 |
53 | useEffect(() => {
54 | let dataObject = {};
55 | for (const keyValuePair of data) {
56 | dataObject[keyValuePair[0]] = keyValuePair[1];
57 | }
58 | change(source, dataObject);
59 | }, [data, change, source]);
60 |
61 | const changeKey = (event, index) => {
62 | const {
63 | target: { value },
64 | } = event;
65 | let newData = [...data];
66 | newData[index][0] = value;
67 | setData(newData);
68 | };
69 |
70 | const changeValue = (event, index) => {
71 | const {
72 | target: { value },
73 | } = event;
74 | let newData = [...data];
75 | newData[index][1] = value;
76 | setData(newData);
77 | };
78 |
79 | const removeKey = index => {
80 | let newData = [...data];
81 | newData.splice(index, 1);
82 | setData(newData);
83 | };
84 |
85 | const addKey = () => {
86 | let newData = [...data];
87 | newData.push(['', '']);
88 | setData(newData);
89 | };
90 |
91 | const isUnique = value => {
92 | let count = 0;
93 | for (const keyValuePair of data) {
94 | if (keyValuePair[0] === value) count++;
95 | }
96 | return count <= 1;
97 | };
98 |
99 | if (data) {
100 | if (keys) {
101 | return (
102 | <>
103 |
104 |
105 |
111 |
112 |
113 |
114 |
115 | Name
116 | Value
117 |
118 |
119 |
120 |
121 | {keys.map((key, index) => (
122 |
123 |
124 | changeKey(e, index)}
129 | />
130 |
131 |
132 |
136 | changeValue(e, index)
137 | }
138 | />
139 |
140 |
141 | removeKey(index)}
144 | >
145 |
149 |
150 |
151 |
152 | ))}
153 |
154 |
155 |
156 |
157 |
158 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 | >
170 | );
171 | } else {
172 | return null;
173 | }
174 | } else {
175 | return null;
176 | }
177 | };
178 |
179 | export default ObjectInput;
180 |
--------------------------------------------------------------------------------
/Development/src/components/PaginationButtons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ChevronLeft,
4 | ChevronRight,
5 | FirstPage,
6 | LastPage,
7 | } from '@material-ui/icons';
8 | import { Button } from '@material-ui/core';
9 | import { includes, keys } from 'lodash';
10 |
11 | const components = {
12 | prev: ChevronLeft,
13 | next: ChevronRight,
14 | last: LastPage,
15 | first: FirstPage,
16 | };
17 |
18 | export const PaginationButton = ({
19 | pagination,
20 | disabled,
21 | nextPage,
22 | rel,
23 | label = rel,
24 | }) => {
25 | rel.toLowerCase();
26 | const buttons = keys(pagination);
27 |
28 | const enabled = (() => {
29 | if (disabled) return false;
30 | return includes(buttons, rel);
31 | })();
32 |
33 | const getIcon = label => {
34 | const ButtonIcon = components[label];
35 | return ;
36 | };
37 |
38 | return (
39 |
43 | );
44 | };
45 |
46 | const PaginationButtons = props => (
47 | <>
48 |
49 |
50 |
51 |
52 | >
53 | );
54 |
55 | export default PaginationButtons;
56 |
--------------------------------------------------------------------------------
/Development/src/components/ParameterRegisters.js:
--------------------------------------------------------------------------------
1 | import { Tooltip, Typography } from '@material-ui/core';
2 | import { get, map } from 'lodash';
3 | import HintTypography from './HintTypography';
4 | import { FRIENDLY_PARAMETERS, useJSONSetting } from '../settings';
5 |
6 | // const SOME_PARAMETER_REGISTER = {
7 | // 'urn:x-vendor:foo:bar': {
8 | // label: 'Foo Bar',
9 | // versions: ['v1.0', 'v1.1'], // optional
10 | // },
11 | // };
12 |
13 | export const unversionedParameter = param => param.split('/')[0];
14 | export const parameterVersion = param => param.split('/')[1];
15 |
16 | export const parameterAutocompleteProps = register => ({
17 | freeSolo: true,
18 | options: [].concat.apply(
19 | [],
20 | map(register, (info, unversioned) =>
21 | map(
22 | get(info, 'versions') || [''],
23 | version => unversioned + (version ? '/' + version : '')
24 | )
25 | )
26 | ),
27 | renderOption: (option, state) => {
28 | const unversioned = unversionedParameter(option);
29 | const version = parameterVersion(option);
30 | const info = get(register, unversioned);
31 | if (info) {
32 | return info.label + (version ? ' ' + version : '');
33 | } else {
34 | return unversioned + (version ? '/' + version : '');
35 | }
36 | },
37 | });
38 |
39 | export const Parameter = ({ register, value }) => {
40 | const [friendlyFirst] = useJSONSetting(FRIENDLY_PARAMETERS, false);
41 | const unversioned = unversionedParameter(value);
42 | const version = parameterVersion(value);
43 | const unfriendly = unversioned + (version ? '/' + version : '');
44 | const info = get(register, unversioned);
45 | if (info) {
46 | const friendly = info.label + (version ? ' ' + version : '');
47 | return (
48 |
49 |
54 |
55 | {friendlyFirst ? friendly : unfriendly}
56 |
57 |
58 |
59 | );
60 | } else {
61 | return (
62 |
63 | {unfriendly}
64 |
65 | );
66 | }
67 | };
68 |
69 | export const ParameterField = ({ register, record, source }) => (
70 |
71 | );
72 |
73 | ParameterField.defaultProps = {
74 | addLabel: true,
75 | };
76 |
77 | // Device Types in the NMOS Parameter Registers
78 | // see https://github.com/AMWA-TV/nmos-parameter-registers/tree/master/device-types
79 | export const DEVICE_TYPES = {
80 | 'urn:x-nmos:device:generic': {
81 | label: 'Generic Device',
82 | },
83 | 'urn:x-nmos:device:pipeline': {
84 | label: 'Pipeline Device',
85 | },
86 | };
87 |
88 | // Device Control Types in the NMOS Parameter Registers
89 | // see https://github.com/AMWA-TV/nmos-parameter-registers/tree/master/device-control-types
90 | export const CONTROL_TYPES = {
91 | // IS-05
92 | 'urn:x-nmos:control:sr-ctrl': {
93 | label: 'Connection API',
94 | versions: ['v1.1', 'v1.0'],
95 | },
96 | // IS-07
97 | 'urn:x-nmos:control:events': {
98 | label: 'Events API',
99 | versions: ['v1.0'],
100 | },
101 | // IS-08
102 | 'urn:x-nmos:control:cm-ctrl': {
103 | label: 'Channel Mapping API',
104 | versions: ['v1.0'],
105 | },
106 | // Manifest Base
107 | 'urn:x-nmos:control:manifest-base': {
108 | label: 'Manifest Base',
109 | versions: ['v1.0'],
110 | },
111 | };
112 |
113 | // Formats in the NMOS Parameter Registers
114 | // see https://github.com/AMWA-TV/nmos-parameter-registers/tree/master/formats
115 | export const FORMATS = {
116 | 'urn:x-nmos:format:video': {
117 | label: 'Video',
118 | },
119 | 'urn:x-nmos:format:audio': {
120 | label: 'Audio',
121 | },
122 | 'urn:x-nmos:format:data': {
123 | label: 'Data',
124 | },
125 | 'urn:x-nmos:format:mux': {
126 | label: 'Multiplexed',
127 | },
128 | };
129 |
130 | // Tags in the NMOS Parameter Registers
131 | // see https://github.com/AMWA-TV/nmos-parameter-registers/tree/master/tags
132 | export const TAGS = {
133 | 'urn:x-nmos:tag:grouphint': {
134 | label: 'Group Hint',
135 | versions: ['v1.0'],
136 | },
137 | // Work-in-progress BCP-002-02 Asset Distinguishing Information
138 | // See https://specs.amwa.tv/bcp-002-02/
139 | 'urn:x-nmos:tag:asset:manufacturer': {
140 | label: 'Manufacturer',
141 | versions: ['v1.0'],
142 | },
143 | 'urn:x-nmos:tag:asset:product': {
144 | label: 'Product Name',
145 | versions: ['v1.0'],
146 | },
147 | 'urn:x-nmos:tag:asset:instance-id': {
148 | label: 'Instance Identifier',
149 | versions: ['v1.0'],
150 | },
151 | 'urn:x-nmos:tag:asset:function': {
152 | label: 'Function',
153 | versions: ['v1.0'],
154 | },
155 | };
156 |
157 | // Transports in the NMOS Parameter Registers
158 | // see https://github.com/AMWA-TV/nmos-parameter-registers/tree/master/transports
159 | export const TRANSPORTS = {
160 | 'urn:x-nmos:transport:rtp': {
161 | label: 'RTP',
162 | },
163 | 'urn:x-nmos:transport:rtp.mcast': {
164 | label: 'RTP Multicast',
165 | },
166 | 'urn:x-nmos:transport:rtp.ucast': {
167 | label: 'RTP Unicast',
168 | },
169 | 'urn:x-nmos:transport:dash': {
170 | label: 'DASH',
171 | },
172 | 'urn:x-nmos:transport:mqtt': {
173 | label: 'MQTT',
174 | },
175 | 'urn:x-nmos:transport:websocket': {
176 | label: 'WebSocket',
177 | },
178 | };
179 |
--------------------------------------------------------------------------------
/Development/src/components/RateField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import get from 'lodash/get';
3 | import { Typography } from '@material-ui/core';
4 |
5 | const RateField = ({ record, source }) => {
6 | const rate = get(record, source);
7 | if (!rate) {
8 | return ;
9 | }
10 | return (
11 |
12 | {rate.numerator} : {rate.denominator ? rate.denominator : 1}
13 |
14 | );
15 | };
16 |
17 | RateField.defaultProps = {
18 | addLabel: true,
19 | };
20 |
21 | export default RateField;
22 |
--------------------------------------------------------------------------------
/Development/src/components/RawButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from 'react-admin';
3 | import JsonIcon from '../icons/JsonIcon';
4 | import { resourceUrl } from '../dataProvider';
5 |
6 | const RawButton = ({ record, resource }) => {
7 | const url = resourceUrl(resource, `/${record.id}`);
8 | return (
9 |
17 | );
18 | };
19 |
20 | export default RawButton;
21 |
--------------------------------------------------------------------------------
/Development/src/components/ResourceShowActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ListButton, TopToolbar } from 'react-admin';
3 | import RawButton from './RawButton';
4 |
5 | const ResourceShowActions = ({ basePath, data, resource }) => (
6 |
7 | {data ? : null}
8 |
9 |
10 | );
11 |
12 | export default ResourceShowActions;
13 |
--------------------------------------------------------------------------------
/Development/src/components/ResourceTitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useResourceContext } from 'react-admin';
3 | import inflection from 'inflection';
4 |
5 | const ResourceTitle = ({ resourceName, recordLabel, record }) => {
6 | const resource = useResourceContext();
7 | if (!resourceName) {
8 | resourceName = inflection.transform(resource, [
9 | 'singularize',
10 | 'titleize',
11 | ]);
12 | }
13 | if (!recordLabel) {
14 | recordLabel = record.label || record.name || record.id;
15 | }
16 | return (
17 |
18 | {resourceName}
19 | {recordLabel ? ': ' + recordLabel : ''}
20 |
21 | );
22 | };
23 |
24 | export default ResourceTitle;
25 |
--------------------------------------------------------------------------------
/Development/src/components/SanitizedDivider.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Divider } from '@material-ui/core';
3 | import sanitizeRestProps from './sanitizeRestProps';
4 |
5 | // Passing react-admin props causes console spam
6 | export const SanitizedDivider = ({ ...rest }) => (
7 |
8 | );
9 |
10 | export default SanitizedDivider;
11 |
--------------------------------------------------------------------------------
/Development/src/components/TAIField.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from '@material-ui/core';
2 | import HintTypography from './HintTypography';
3 | import tai from 't-a-i';
4 | import dayjs from 'dayjs';
5 | import utc from 'dayjs/plugin/utc';
6 | import get from 'lodash/get';
7 | dayjs.extend(utc);
8 |
9 | const TAIField = ({ record, source, mode }) => (
10 |
11 |
17 |
18 | {get(record, source)}
19 |
20 |
21 |
22 | );
23 |
24 | const TAIConversion = (record, source, mode) => {
25 | try {
26 | const taiData = get(record, source).split(':', 2);
27 | if (!taiData[0] || !taiData[1]) return '';
28 | const taiTimeMilliseconds = 1e3 * taiData[0] + taiData[1] / 1e6;
29 | if (get(record, mode) === 'activate_scheduled_relative') {
30 | return dayjs(taiTimeMilliseconds).utc().format('HH:mm:ss.SSS');
31 | }
32 | const unixTimeMilliseconds = tai.atomicToUnix(taiTimeMilliseconds);
33 | return dayjs(unixTimeMilliseconds).format('YYYY-MM-DD HH:mm:ss.SSS Z');
34 | } catch (e) {
35 | return `Conversion Error - ${e}`;
36 | }
37 | };
38 |
39 | TAIField.defaultProps = {
40 | addLabel: true,
41 | };
42 |
43 | export default TAIField;
44 |
--------------------------------------------------------------------------------
/Development/src/components/TransportFileViewer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardContent, IconButton, Typography } from '@material-ui/core';
3 | import { Labeled, useNotify } from 'react-admin';
4 | import copy from 'clipboard-copy';
5 | import get from 'lodash/get';
6 | import { ContentCopyIcon } from '../icons';
7 |
8 | const TransportFileViewer = ({ endpoint, ...props }) => {
9 | const notify = useNotify();
10 | const handleCopy = () => {
11 | copy(get(props.record, `${endpoint}`)).then(() => {
12 | notify('Transport file copied');
13 | });
14 | };
15 |
16 | if (!get(props.record, `${endpoint}`)) {
17 | return null;
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 | {get(props.record, `${endpoint}`)}
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default TransportFileViewer;
43 |
--------------------------------------------------------------------------------
/Development/src/components/URLField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography } from '@material-ui/core';
3 | import get from 'lodash/get';
4 |
5 | const URLField = ({ record, source }) => (
6 |
12 |
18 | {get(record, source)}
19 |
20 |
21 | );
22 | URLField.defaultProps = {
23 | addLabel: true,
24 | };
25 |
26 | export default URLField;
27 |
--------------------------------------------------------------------------------
/Development/src/components/UnsortableDatagrid.js:
--------------------------------------------------------------------------------
1 | import React, { Children, cloneElement, isValidElement } from 'react';
2 | import { Datagrid } from 'react-admin';
3 |
4 | export const UnsortableDatagrid = ({ children, ...props }) => (
5 |
6 | {Children.map(
7 | children,
8 | child =>
9 | isValidElement(child) &&
10 | cloneElement(child, { sortable: false })
11 | )}
12 |
13 | );
14 |
15 | export default UnsortableDatagrid;
16 |
--------------------------------------------------------------------------------
/Development/src/components/labelize.js:
--------------------------------------------------------------------------------
1 | // some abbreviations used in IS-04 and IS-05 APIs
2 | // or the nmos-cpp Logging API
3 | const abbreviations = {
4 | api: 'API',
5 | caps: 'Capabilities',
6 | ext: '(Ext)',
7 | fec: 'FEC',
8 | fec1d: 'FEC1D',
9 | fec2d: 'FEC2D',
10 | href: 'Address',
11 | http: 'HTTP',
12 | id: 'ID',
13 | ip: 'IP',
14 | is: 'IS',
15 | mqtt: 'MQTT',
16 | ms: '(ms)',
17 | ptp: 'PTP',
18 | rest: 'REST',
19 | rtcp: 'RTCP',
20 | rtp: 'RTP',
21 | uri: 'URI',
22 | url: 'URL',
23 | ws: 'WebSocket',
24 | };
25 |
26 | const labelize = source => {
27 | // '/meow.$purr.hiss_yowl_ms' => 'Meow Purr Hiss Yowl (ms)'
28 | const label = source
29 | .replace(/[ /$._]+/g, ' ')
30 | .trim()
31 | .replace(
32 | /\S+/g,
33 | word =>
34 | abbreviations[word.toLowerCase()] ||
35 | word.charAt(0).toUpperCase() + word.substr(1).toLowerCase()
36 | );
37 | return label;
38 | };
39 |
40 | export default labelize;
41 |
--------------------------------------------------------------------------------
/Development/src/components/sanitizeRestProps.js:
--------------------------------------------------------------------------------
1 | import omit from 'lodash/omit';
2 |
3 | const sanitizeRestProps = props =>
4 | omit(props, [
5 | 'addLabel',
6 | 'allowEmpty',
7 | 'basePath',
8 | 'cellClassName',
9 | 'className',
10 | 'formClassName',
11 | 'headerClassName',
12 | 'label',
13 | 'linkType',
14 | 'link',
15 | 'locale',
16 | 'record',
17 | 'resource',
18 | 'sortable',
19 | 'sortBy',
20 | 'source',
21 | 'textAlign',
22 | 'translateChoice',
23 | ]);
24 |
25 | export default sanitizeRestProps;
26 |
--------------------------------------------------------------------------------
/Development/src/components/useCustomNamesContext.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import CustomNamesContext from './CustomNamesContext';
3 |
4 | export const useCustomNamesContext = () => useContext(CustomNamesContext);
5 | export default useCustomNamesContext;
6 |
--------------------------------------------------------------------------------
/Development/src/components/useDebounce.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useRef, useState } from 'react';
2 | import debounce from 'lodash/debounce';
3 |
4 | export const useDebouncedCallback = (callback, delay) =>
5 | useMemo(() => debounce(callback, delay), [callback, delay]);
6 |
7 | const useDebounce = (value, delay) => {
8 | const previousValue = useRef(value);
9 | const [currentValue, setCurrentValue] = useState(value);
10 | const debouncedCallback = useDebouncedCallback(
11 | value => setCurrentValue(value),
12 | delay
13 | );
14 | useEffect(() => {
15 | if (value !== previousValue.current) {
16 | debouncedCallback(value);
17 | previousValue.current = value;
18 | }
19 | }, [debouncedCallback, value]);
20 | return currentValue;
21 | };
22 |
23 | export default useDebounce;
24 |
--------------------------------------------------------------------------------
/Development/src/components/useGetList.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { shallowEqual, useSelector } from 'react-redux';
3 | import {
4 | CRUD_GET_LIST,
5 | useCheckMinimumRequiredProps,
6 | useDataProvider,
7 | useNotify,
8 | useSafeSetState,
9 | useVersion,
10 | } from 'react-admin';
11 | import isEqual from 'lodash/isEqual';
12 | import useDebounce from './useDebounce';
13 |
14 | const isEmptyList = data =>
15 | Array.isArray(data)
16 | ? data.length === 0
17 | : data &&
18 | Object.keys(data).length === 0 &&
19 | data.hasOwnProperty('fetchedAt');
20 |
21 | // We need a custom hook as the request URL needs to be returned.
22 | const useQueryWithStore = (query, options, dataSelector, totalSelector) => {
23 | const { type, resource, payload } = query;
24 | const data = useSelector(dataSelector);
25 | const total = useSelector(totalSelector);
26 | const [state, setState] = useSafeSetState({
27 | data,
28 | total,
29 | error: null,
30 | loading: true,
31 | loaded: data !== undefined && !isEmptyList(data),
32 | pagination: null,
33 | url: null,
34 | });
35 | if (!isEqual(state.data, data) || state.total !== total) {
36 | setState(
37 | Object.assign(Object.assign({}, state), {
38 | data,
39 | total,
40 | loaded: true,
41 | })
42 | );
43 | }
44 | const dataProvider = useDataProvider();
45 | useEffect(() => {
46 | // If the filter has changed ignore paginationURL
47 | payload.paginationURL = null;
48 | }, [payload.filter]); // eslint-disable-line
49 | useEffect(() => {
50 | setState(prevState =>
51 | Object.assign(Object.assign({}, prevState), { loading: true })
52 | );
53 | dataProvider[type](resource, payload, options)
54 | .then(response => {
55 | // We only care about the dataProvider url response here, because
56 | // the list data was already passed to the SUCCESS redux reducer.
57 | setState(prevState =>
58 | Object.assign(Object.assign({}, prevState), {
59 | error: null,
60 | loading: false,
61 | loaded: true,
62 | pagination: response.pagination,
63 | url: response.url,
64 | })
65 | );
66 | })
67 | .catch(error => {
68 | setState({
69 | error,
70 | loading: false,
71 | loaded: false,
72 | });
73 | });
74 | }, [JSON.stringify({ query, options })]); // eslint-disable-line
75 | return state;
76 | };
77 |
78 | const useGetList = props => {
79 | useCheckMinimumRequiredProps(
80 | 'List',
81 | ['basePath', 'filter', 'resource'],
82 | props
83 | );
84 | const { basePath, resource, paginationURL, filter } = props;
85 | const debouncedFilter = useDebounce(filter, 250);
86 |
87 | const notify = useNotify();
88 | const version = useVersion();
89 |
90 | const {
91 | total,
92 | error,
93 | loading,
94 | loaded,
95 | pagination,
96 | url,
97 | } = useQueryWithStore(
98 | {
99 | type: 'getList',
100 | resource,
101 | payload: {
102 | filter: debouncedFilter,
103 | paginationURL,
104 | },
105 | },
106 | {
107 | action: CRUD_GET_LIST,
108 | version,
109 | onFailure: error =>
110 | notify(
111 | typeof error === 'string'
112 | ? error
113 | : error.message || 'ra.notification.http_error',
114 | 'warning'
115 | ),
116 | },
117 | state =>
118 | state.admin.resources[resource]
119 | ? state.admin.resources[resource].list.ids
120 | : null,
121 | state =>
122 | state.admin.resources[resource]
123 | ? state.admin.resources[resource].list.total
124 | : null
125 | );
126 | const data = useSelector(
127 | state =>
128 | state.admin.resources[resource]
129 | ? state.admin.resources[resource].data
130 | : {},
131 | shallowEqual
132 | );
133 | const ids = useSelector(
134 | state =>
135 | state.admin.resources[resource]
136 | ? state.admin.resources[resource].list.ids
137 | : [],
138 | shallowEqual
139 | );
140 |
141 | const listDataObject = {};
142 | ids.forEach(key => (listDataObject[key] = data[key]));
143 |
144 | const listDataArray = Object.keys(listDataObject).map(key => {
145 | return listDataObject[key];
146 | });
147 |
148 | return {
149 | basePath,
150 | data: listDataArray,
151 | error,
152 | ids,
153 | loading,
154 | loaded,
155 | pagination,
156 | resource,
157 | total,
158 | url,
159 | version,
160 | };
161 | };
162 |
163 | export default useGetList;
164 |
--------------------------------------------------------------------------------
/Development/src/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "nmos-js",
3 | "about": "An NMOS Client",
4 | "palette": {
5 | "primary": {
6 | "main": "rgb(45,117,199)",
7 | "contrastText": "#fff"
8 | },
9 | "secondary": {
10 | "main": "rgb(0,47,103)",
11 | "contrastText": "#fff"
12 | }
13 | },
14 | "Query API": {
15 | },
16 | "DNS-SD API": {
17 | },
18 | "Logging API": {
19 | },
20 | "RQL": {
21 | "value": true,
22 | "disabled": true
23 | },
24 | "Paging Limit": {
25 | },
26 | "Friendly Parameters": {
27 | "value": true
28 | },
29 | "Connect Filter": {
30 | "value": {
31 | "$constraint_sets_active": null
32 | }
33 | },
34 | "refresh time": {
35 | "value": 4
36 | },
37 | "theme": {
38 | "value": {
39 | "type": "light"
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Development/src/icons/ActivateImmediate.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgActivateImmediate = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgActivateImmediate;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/ActivateScheduled.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgActivateScheduled = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgActivateScheduled;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/CancelScheduledActivation.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgCancelScheduledActivation = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgCancelScheduledActivation;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/ConnectRegistry.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgConnectRegistry = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgConnectRegistry;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/ContentCopy.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgContentCopy = props => (
5 |
6 |
7 |
8 | );
9 |
10 | export default SvgContentCopy;
11 |
--------------------------------------------------------------------------------
/Development/src/icons/Device.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgDevice = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgDevice;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/Flow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgFlow = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgFlow;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/JsonIcon.js:
--------------------------------------------------------------------------------
1 | let _interopRequireDefault = require('@babel/runtime/helpers/interopRequireDefault');
2 |
3 | Object.defineProperty(exports, '__esModule', {
4 | value: true,
5 | });
6 | exports.default = void 0;
7 |
8 | const _react = _interopRequireDefault(require('react'));
9 |
10 | const _createSvgIcon = _interopRequireDefault(
11 | require('@material-ui/icons/utils/createSvgIcon')
12 | );
13 |
14 | const _default = (0, _createSvgIcon.default)(
15 | _react.default.createElement(
16 | 'g',
17 | null,
18 | _react.default.createElement(
19 | 'style',
20 | null,
21 | '.txt { font-size: 14px; font-family: monospace; }'
22 | ),
23 | _react.default.createElement(
24 | 'text',
25 | {
26 | x: 0,
27 | y: 15,
28 | className: 'txt',
29 | },
30 | '{\u2026}'
31 | )
32 | ),
33 | 'json'
34 | );
35 |
36 | exports.default = _default;
37 |
--------------------------------------------------------------------------------
/Development/src/icons/Node.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgNode = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgNode;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/Receiver.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgReceiver = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgReceiver;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/Registry.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgRegistry = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgRegistry;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/RegistryLogs.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgRegistryLogs = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgRegistryLogs;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/Sender.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgSender = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgSender;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/Source.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgSource = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgSource;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/Stage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgStage = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgStage;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/Subscription.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgSubscription = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgSubscription;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import ActivateImmediateIcon from './ActivateImmediate';
2 | import ActivateScheduledIcon from './ActivateScheduled';
3 | import CancelScheduledActivationIcon from './CancelScheduledActivation';
4 | import ConnectRegistryIcon from './ConnectRegistry';
5 | import ContentCopyIcon from './ContentCopy';
6 | import DeviceIcon from './Device';
7 | import FlowIcon from './Flow';
8 | import JsonIcon from './JsonIcon';
9 | import NodeIcon from './Node';
10 | import ReceiverIcon from './Receiver';
11 | import RegistryIcon from './Registry';
12 | import RegistryLogsIcon from './RegistryLogs';
13 | import SenderIcon from './Sender';
14 | import SourceIcon from './Source';
15 | import StageIcon from './Stage';
16 | import SubscriptionIcon from './Subscription';
17 |
18 | // To optimise the SVG files and convert to the correct syntax:
19 | // svgo --enable=removeDimensions --disable=removeViewBox --pretty -f .\src\ -o .\optimised\
20 | // npx @svgr/cli -d ./out/ --template ./svgr-template.js ./optimised/
21 |
22 | export {
23 | ActivateImmediateIcon,
24 | ActivateScheduledIcon,
25 | CancelScheduledActivationIcon,
26 | ConnectRegistryIcon,
27 | ContentCopyIcon,
28 | DeviceIcon,
29 | FlowIcon,
30 | JsonIcon,
31 | NodeIcon,
32 | ReceiverIcon,
33 | RegistryIcon,
34 | RegistryLogsIcon,
35 | SenderIcon,
36 | SourceIcon,
37 | StageIcon,
38 | SubscriptionIcon,
39 | };
40 |
--------------------------------------------------------------------------------
/Development/src/icons/svgr-template.js:
--------------------------------------------------------------------------------
1 | function template(
2 | { template },
3 | opts,
4 | { imports, componentName, props, jsx, exports }
5 | ) {
6 | jsx.openingElement.name.name = 'SvgIcon';
7 | jsx.closingElement.name.name = 'SvgIcon';
8 | jsx.openingElement.attributes.push({
9 | type: 'JSXSpreadAttribute',
10 | argument: {
11 | type: 'Identifier',
12 | name: 'props',
13 | },
14 | });
15 | return template.ast`
16 | ${imports}
17 | import { SvgIcon } from '@material-ui/core';
18 | const ${componentName} = (${props}) => ${jsx}
19 | ${exports}
20 | `;
21 | }
22 | module.exports = template;
23 |
--------------------------------------------------------------------------------
/Development/src/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow-y: scroll;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | padding: 0;
8 | font-family: sans-serif;
9 | }
10 |
--------------------------------------------------------------------------------
/Development/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 | import { AppThemeProvider } from './theme/ThemeContext';
7 |
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.getElementById('root')
13 | );
14 | registerServiceWorker();
15 |
--------------------------------------------------------------------------------
/Development/src/pages/about.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardContent, Typography } from '@material-ui/core';
3 | import { get } from 'lodash';
4 | import { Title } from 'react-admin';
5 | import CONFIG from '../config.json';
6 | import AppLogo from '../components/AppLogo';
7 |
8 | export const About = () => (
9 |
10 |
11 |
12 |
13 |
14 | {get(CONFIG, 'about', 'An NMOS Client')}
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
23 | export const RepoLinks = () => (
24 |
25 |
26 | Built on open-source projects
27 | {' '}
28 |
29 |
30 | nmos-js
31 |
32 | {' '}
33 |
34 | and
35 | {' '}
36 |
37 |
38 | nmos-cpp
39 |
40 |
41 |
42 | );
43 |
44 | const Link = ({ to, children }) => (
45 | // eslint-disable-next-line react/jsx-no-target-blank
46 |
52 | {React.cloneElement(children, {
53 | style: { textDecoration: 'underline' },
54 | })}
55 |
56 | );
57 |
58 | export default About;
59 |
--------------------------------------------------------------------------------
/Development/src/pages/devices/index.js:
--------------------------------------------------------------------------------
1 | import DevicesList from './DevicesList';
2 | import DevicesShow from './DevicesShow';
3 |
4 | export { DevicesList, DevicesShow };
5 |
--------------------------------------------------------------------------------
/Development/src/pages/flows/FlowsList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import FilterPanel, {
13 | AutocompleteFilter,
14 | RateFilter,
15 | StringFilter,
16 | } from '../../components/FilterPanel';
17 | import {
18 | FORMATS,
19 | ParameterField,
20 | parameterAutocompleteProps,
21 | } from '../../components/ParameterRegisters';
22 | import PaginationButtons from '../../components/PaginationButtons';
23 | import ListActions from '../../components/ListActions';
24 | import useGetList from '../../components/useGetList';
25 | import { queryVersion, useJSONSetting } from '../../settings';
26 |
27 | const FlowsList = props => {
28 | const [filter, setFilter] = useJSONSetting('Flows Filter');
29 | const [paginationURL, setPaginationURL] = useState(null);
30 | const { data, loaded, pagination, url } = useGetList({
31 | ...props,
32 | filter,
33 | paginationURL,
34 | });
35 | if (!loaded) return ;
36 |
37 | const nextPage = label => {
38 | setPaginationURL(pagination[label]);
39 | };
40 |
41 | return (
42 | <>
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {queryVersion() >= 'v1.1' && (
54 |
55 | )}
56 |
60 | {queryVersion() >= 'v1.1' && (
61 |
62 | )}
63 | {queryVersion() >= 'v1.1' && (
64 |
65 | )}
66 | {queryVersion() >= 'v1.3' && (
67 |
72 | )}
73 |
74 |
75 |
76 |
77 |
78 |
83 | Label
84 |
85 | Format
86 | {queryVersion() >= 'v1.1' && (
87 | Media Type
88 | )}
89 | {queryVersion() >= 'v1.3' && (
90 | Event Type
91 | )}
92 |
93 |
94 |
95 | {data.map(item => (
96 |
97 |
98 |
106 |
107 |
108 |
113 |
114 | {queryVersion() >= 'v1.1' && (
115 | {item.media_type}
116 | )}
117 | {queryVersion() >= 'v1.3' && (
118 | {item.event_type}
119 | )}
120 |
121 | ))}
122 |
123 |
124 |
125 |
130 |
131 |
132 | >
133 | );
134 | };
135 |
136 | const eventTypes = ['boolean', 'string', 'number'];
137 |
138 | export default FlowsList;
139 |
--------------------------------------------------------------------------------
/Development/src/pages/flows/index.js:
--------------------------------------------------------------------------------
1 | import FlowsList from './FlowsList';
2 | import FlowsShow from './FlowsShow';
3 |
4 | export { FlowsList, FlowsShow };
5 |
--------------------------------------------------------------------------------
/Development/src/pages/logs/LogsList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import FilterPanel, {
13 | AutocompleteFilter,
14 | NumberFilter,
15 | StringFilter,
16 | } from '../../components/FilterPanel';
17 | import PaginationButtons from '../../components/PaginationButtons';
18 | import ListActions from '../../components/ListActions';
19 | import useGetList from '../../components/useGetList';
20 | import { useJSONSetting } from '../../settings';
21 |
22 | const LogsList = props => {
23 | const [filter, setFilter] = useJSONSetting('Logs Filter');
24 | const [paginationURL, setPaginationURL] = useState(null);
25 | const { data, loaded, pagination, url } = useGetList({
26 | ...props,
27 | filter,
28 | paginationURL,
29 | });
30 | if (!loaded) return ;
31 |
32 | const nextPage = label => {
33 | setPaginationURL(pagination[label]);
34 | };
35 |
36 | return (
37 | <>
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
55 |
56 |
57 |
58 |
59 |
64 | Timestamp
65 |
66 | Level
67 | Message
68 | Request URI
69 | HTTP Method
70 |
71 |
72 |
73 | {data.map(item => (
74 |
75 |
76 |
84 |
85 | {item.level}
86 | {item.message}
87 | {item.request_uri}
88 | {item.http_method}
89 |
90 | ))}
91 |
92 |
93 |
94 |
99 |
100 |
101 | >
102 | );
103 | };
104 |
105 | const httpMethods = ['GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'];
106 |
107 | export default LogsList;
108 |
--------------------------------------------------------------------------------
/Development/src/pages/logs/LogsShow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ShowContextProvider,
4 | ShowView,
5 | SimpleShowLayout,
6 | TextField,
7 | useRecordContext,
8 | useShowController,
9 | } from 'react-admin';
10 | import get from 'lodash/get';
11 | import ObjectField from '../../components/ObjectField';
12 | import ResourceShowActions from '../../components/ResourceShowActions';
13 | import ResourceTitle from '../../components/ResourceTitle';
14 | import SanitizedDivider from '../../components/SanitizedDivider';
15 |
16 | export const LogsShow = props => {
17 | const controllerProps = useShowController(props);
18 | return (
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | const LogsShowView = props => {
26 | const { record } = useRecordContext();
27 | return (
28 | }
30 | actions={}
31 | {...props}
32 | >
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default LogsShow;
57 |
--------------------------------------------------------------------------------
/Development/src/pages/logs/index.js:
--------------------------------------------------------------------------------
1 | import LogsList from './LogsList';
2 | import LogsShow from './LogsShow';
3 |
4 | export { LogsList, LogsShow };
5 |
--------------------------------------------------------------------------------
/Development/src/pages/menu.js:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useEffect } from 'react';
2 | import { NavLink, useHistory } from 'react-router-dom';
3 | import {
4 | Divider,
5 | ListItemIcon,
6 | ListItemText,
7 | MenuItem,
8 | MenuList,
9 | } from '@material-ui/core';
10 | import HomeIcon from '@material-ui/icons/Home';
11 | import SettingsIcon from '@material-ui/icons/Settings';
12 | import { makeStyles } from '@material-ui/styles';
13 |
14 | import {
15 | DeviceIcon,
16 | FlowIcon,
17 | NodeIcon,
18 | ReceiverIcon,
19 | RegistryIcon,
20 | RegistryLogsIcon,
21 | SenderIcon,
22 | SourceIcon,
23 | SubscriptionIcon,
24 | } from '../icons';
25 |
26 | import labelize from '../components/labelize';
27 |
28 | const NavLinkRef = forwardRef((props, ref) => (
29 |
30 | ));
31 |
32 | const useStyles = makeStyles(theme => ({
33 | root: {
34 | paddingLeft: '24px',
35 | borderLeftWidth: '3px',
36 | borderLeftStyle: 'solid',
37 | borderLeftColor: 'transparent',
38 | },
39 | active: {
40 | borderLeftColor: theme.palette.primary.main,
41 | color: theme.palette.primary.main,
42 | '& $icon': {
43 | color: theme.palette.primary.main,
44 | },
45 | },
46 | icon: {},
47 | }));
48 |
49 | const CustomMenuItem = ({ to, icon, label = labelize(to), ...props }) => {
50 | const classes = useStyles();
51 | return (
52 |
65 | );
66 | };
67 |
68 | const CustomMenu = () => {
69 | const history = useHistory();
70 | useEffect(() => {
71 | history.block(
72 | (location, action) =>
73 | !(
74 | action === 'PUSH' &&
75 | location.pathname === history.location.pathname
76 | )
77 | );
78 | }, [history]);
79 | return (
80 |
81 | } label={'Home'} />
82 | } />
83 |
84 | } />
85 | } />
86 | } />
87 | } />
88 | } />
89 | } />
90 | } />
91 |
92 | }
95 | label="Query APIs"
96 | />
97 | } />
98 |
99 | );
100 | };
101 |
102 | export default CustomMenu;
103 |
--------------------------------------------------------------------------------
/Development/src/pages/nodes/NodesShow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ArrayField,
4 | BooleanField,
5 | FunctionField,
6 | ReferenceManyField,
7 | ShowContextProvider,
8 | ShowView,
9 | SimpleShowLayout,
10 | SingleFieldList,
11 | TextField,
12 | useShowController,
13 | } from 'react-admin';
14 | import LinkIcon from '@material-ui/icons/Link';
15 | import LinkChipField from '../../components/LinkChipField';
16 | import ItemArrayField from '../../components/ItemArrayField';
17 | import ObjectField from '../../components/ObjectField';
18 | import { TAGS } from '../../components/ParameterRegisters';
19 | import ResourceShowActions from '../../components/ResourceShowActions';
20 | import ResourceTitle from '../../components/ResourceTitle';
21 | import TAIField from '../../components/TAIField';
22 | import SanitizedDivider from '../../components/SanitizedDivider';
23 | import UnsortableDatagrid from '../../components/UnsortableDatagrid';
24 | import UrlField from '../../components/URLField';
25 | import { queryVersion } from '../../settings';
26 |
27 | function buildLink(record) {
28 | return record.protocol + '://' + record.host + ':' + record.port;
29 | }
30 |
31 | export const NodesShow = props => {
32 | const controllerProps = useShowController(props);
33 | return (
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | const NodesShowView = props => {
41 | return (
42 | }
45 | actions={}
46 | >
47 |
48 |
49 |
50 |
51 | {queryVersion() >= 'v1.1' && }
52 | {queryVersion() >= 'v1.1' && (
53 |
54 | )}
55 |
56 |
57 |
58 | {queryVersion() >= 'v1.1' && (
59 |
63 | )}
64 | {queryVersion() >= 'v1.1' && (
65 |
66 |
67 |
68 |
69 |
70 | {queryVersion() >= 'v1.3' && (
71 |
72 | )}
73 | (
75 |
80 |
84 |
85 | )}
86 | />
87 |
88 |
89 | )}
90 | {queryVersion() >= 'v1.1' && (
91 |
92 |
93 |
94 |
95 |
96 |
97 | )}
98 |
99 |
100 |
101 |
102 | {queryVersion() >= 'v1.3' && (
103 |
104 | )}
105 |
106 |
107 | {queryVersion() >= 'v1.2' && (
108 |
109 |
110 |
111 |
115 |
116 | {queryVersion() >= 'v1.3' && (
117 |
121 | )}
122 | {queryVersion() >= 'v1.3' && (
123 |
127 | )}
128 |
129 |
130 | )}
131 |
132 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | );
145 | };
146 |
147 | export default NodesShow;
148 |
--------------------------------------------------------------------------------
/Development/src/pages/nodes/index.js:
--------------------------------------------------------------------------------
1 | import NodesList from './NodesList';
2 | import NodesShow from './NodesShow';
3 |
4 | export { NodesList, NodesShow };
5 |
--------------------------------------------------------------------------------
/Development/src/pages/queryapis/ConnectButton.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Menu, MenuItem } from '@material-ui/core';
3 | import { useNotify } from 'react-admin';
4 | import get from 'lodash/get';
5 | import { QUERY_API, apiUrl, disabledSetting, setApiUrl } from '../../settings';
6 | import { ConnectRegistryIcon } from '../../icons';
7 |
8 | const ConnectButton = ({ record, variant = 'contained', size }) => {
9 | const [anchorEl, setAnchorEl] = useState(null);
10 | const notify = useNotify();
11 |
12 | if (!record) {
13 | return null;
14 | }
15 |
16 | const scheme = get(record, 'txt.api_proto');
17 |
18 | const makeQueryAPI = selectedAddress => {
19 | return (
20 | scheme +
21 | '://' +
22 | selectedAddress +
23 | ':' +
24 | get(record, 'port') +
25 | '/x-nmos/query/' +
26 | get(record, 'txt.api_ver').split(',').slice(-1)[0]
27 | );
28 | };
29 |
30 | const changeQueryAPI = selectedAddress => {
31 | setApiUrl(QUERY_API, makeQueryAPI(selectedAddress));
32 | };
33 |
34 | // heuristic decision about whether to give the user a single-click connect
35 | // or whether to pop up a menu of hosts to select from
36 | const hosts = (() => {
37 | const hostname = get(record, 'host_target');
38 | const addresses = get(record, 'addresses');
39 | if (!hostname.endsWith('local.')) {
40 | // i.e. unicast DNS-SD hostname
41 | return [hostname];
42 | } else if (scheme === 'http' && addresses.length === 1) {
43 | // i.e. mDNS hostname resolved to a single address used with HTTP
44 | return [addresses[0]];
45 | } else {
46 | // i.e. mDNS hostname resolved to multiple addresses or used with HTTPS
47 | return [hostname, ...addresses];
48 | }
49 | })();
50 |
51 | const handleButtonClick = event => {
52 | if (hosts.length > 1) {
53 | setAnchorEl(event.currentTarget);
54 | } else {
55 | changeQueryAPI(hosts[0]);
56 | notify(`Connected to: ${apiUrl(QUERY_API)}`);
57 | }
58 | };
59 |
60 | const handleMenuItemClick = option => {
61 | changeQueryAPI(option);
62 | notify(`Connected to: ${apiUrl(QUERY_API)}`);
63 | setAnchorEl(null);
64 | };
65 |
66 | return (
67 | <>
68 | }
72 | variant={variant}
73 | size={
74 | size ? size : variant === 'contained' ? 'medium' : 'small'
75 | }
76 | disabled={disabledSetting(QUERY_API)}
77 | >
78 | Connect
79 |
80 |
105 | >
106 | );
107 | };
108 |
109 | export default ConnectButton;
110 |
--------------------------------------------------------------------------------
/Development/src/pages/queryapis/QueryAPIsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import FilterPanel, { StringFilter } from '../../components/FilterPanel';
13 | import ListActions from '../../components/ListActions';
14 | import ConnectButton from './ConnectButton';
15 | import useGetList from '../../components/useGetList';
16 | import { useJSONSetting } from '../../settings';
17 |
18 | const QueryAPIsList = props => {
19 | const [filter, setFilter] = useJSONSetting('Query APIs Filter');
20 | const { data, loaded, url } = useGetList({
21 | ...props,
22 | filter,
23 | });
24 | if (!loaded) return ;
25 |
26 | return (
27 | <>
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
44 |
49 | Name
50 |
51 | API Versions
52 | Priority
53 |
54 |
55 |
56 |
57 | {data.map(item => (
58 |
59 |
60 |
68 |
69 | {item.txt.api_ver}
70 | {item.txt.pri}
71 |
72 |
76 |
77 |
78 | ))}
79 |
80 |
81 |
82 |
83 | >
84 | );
85 | };
86 |
87 | export default QueryAPIsList;
88 |
--------------------------------------------------------------------------------
/Development/src/pages/queryapis/QueryAPIsShow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | BooleanField,
4 | FunctionField,
5 | ShowContextProvider,
6 | ShowView,
7 | SimpleShowLayout,
8 | TextField,
9 | Toolbar,
10 | useRecordContext,
11 | useShowController,
12 | } from 'react-admin';
13 | import get from 'lodash/get';
14 | import ItemArrayField from '../../components/ItemArrayField';
15 | import ResourceShowActions from '../../components/ResourceShowActions';
16 | import ResourceTitle from '../../components/ResourceTitle';
17 | import SanitizedDivider from '../../components/SanitizedDivider';
18 | import ConnectButton from './ConnectButton';
19 |
20 | export const QueryAPIsShow = props => {
21 | const controllerProps = useShowController(props);
22 | return (
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | const QueryAPIsShowView = props => {
30 | const { record } = useRecordContext();
31 | return (
32 | <>
33 | }
36 | actions={}
37 | >
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | (
51 |
55 | )}
56 | />
57 |
58 |
59 |
60 |
61 | <>
62 |
63 | >
64 |
65 | >
66 | );
67 | };
68 |
69 | export default QueryAPIsShow;
70 |
--------------------------------------------------------------------------------
/Development/src/pages/queryapis/index.js:
--------------------------------------------------------------------------------
1 | import QueryAPIsList from './QueryAPIsList';
2 | import QueryAPIsShow from './QueryAPIsShow';
3 |
4 | export { QueryAPIsList, QueryAPIsShow };
5 |
--------------------------------------------------------------------------------
/Development/src/pages/receivers/ConnectButtons.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Menu, MenuItem } from '@material-ui/core';
3 | import { useHistory } from 'react-router-dom';
4 | import get from 'lodash/get';
5 | import { useNotify, useRefresh } from 'react-admin';
6 | import makeConnection from '../../components/makeConnection';
7 | import { ActivateImmediateIcon, StageIcon } from '../../icons';
8 | import dataProvider from '../../dataProvider';
9 |
10 | const ConnectButtons = ({ senderData, receiverData }) => {
11 | const [anchorEl, setAnchorEl] = useState(null);
12 | const [senderLegs, setSenderLegs] = useState(0);
13 | const [endpoint, setEndpoint] = useState('');
14 |
15 | const history = useHistory();
16 | const notify = useNotify();
17 | const refresh = useRefresh();
18 |
19 | const connect = (endpoint, senderLeg) => {
20 | const options = { singleSenderLeg: senderLeg };
21 | makeConnection(senderData.id, receiverData.id, endpoint, options)
22 | .then(() => {
23 | notify('Element updated', 'info');
24 | refresh();
25 | history.push(`/receivers/${receiverData.id}/show/${endpoint}`);
26 | })
27 | .catch(error => {
28 | if (error && error.hasOwnProperty('body'))
29 | notify(
30 | get(error.body, 'error') +
31 | ' - ' +
32 | get(error.body, 'code') +
33 | ' - ' +
34 | get(error.body, 'debug'),
35 | 'warning'
36 | );
37 | notify(error.toString(), 'warning');
38 | });
39 | };
40 |
41 | const handleConnect = (endpoint, event) => {
42 | setEndpoint(endpoint);
43 | if (get(receiverData, '$staged.transport_params').length === 1) {
44 | const ref = event.currentTarget;
45 | dataProvider('GET_ONE', 'senders', {
46 | id: senderData.id,
47 | }).then(({ data: senderData }) => {
48 | if (get(senderData, '$staged.transport_params').length > 1) {
49 | setSenderLegs(
50 | get(senderData, '$staged.transport_params').length
51 | );
52 | setAnchorEl(ref);
53 | } else {
54 | connect(endpoint);
55 | }
56 | });
57 | } else {
58 | connect(endpoint);
59 | }
60 | };
61 |
62 | return (
63 | <>
64 |
71 |
78 |
102 | >
103 | );
104 | };
105 |
106 | export default ConnectButtons;
107 |
--------------------------------------------------------------------------------
/Development/src/pages/receivers/ReceiversEdit.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Link, Route } from 'react-router-dom';
3 | import { Paper, Tab, Tabs } from '@material-ui/core';
4 | import {
5 | BooleanInput,
6 | Edit,
7 | FormDataConsumer,
8 | SelectInput,
9 | SimpleForm,
10 | TextInput,
11 | } from 'react-admin';
12 | import get from 'lodash/get';
13 | import set from 'lodash/set';
14 | import ClearIcon from '@material-ui/icons/Clear';
15 | import { useTheme } from '@material-ui/styles';
16 | import ConnectionEditActions from '../../components/ConnectionEditActions';
17 | import ConnectionEditToolbar from '../../components/ConnectionEditToolbar';
18 | import ResourceTitle from '../../components/ResourceTitle';
19 | import ReceiverTransportParamsCardsGrid from './ReceiverTransportParams';
20 |
21 | const ReceiversEdit = props => {
22 | const theme = useTheme();
23 | const tabBackgroundColor =
24 | theme.palette.type === 'light'
25 | ? theme.palette.grey[100]
26 | : theme.palette.grey[900];
27 | return (
28 | <>
29 |
30 |
36 |
41 |
46 |
51 |
57 |
62 |
63 |
64 |
65 |
66 |
67 | }
71 | />
72 | >
73 | );
74 | };
75 |
76 | const EditStagedTab = props => (
77 | }
81 | actions={}
82 | >
83 | }
85 | redirect={`/receivers/${props.id}/show/staged`}
86 | >
87 |
88 |
92 | },
97 | {
98 | id: 'activate_immediate',
99 | name: 'activate_immediate',
100 | },
101 | {
102 | id: 'activate_scheduled_relative',
103 | name: 'activate_scheduled_relative',
104 | },
105 | {
106 | id: 'activate_scheduled_absolute',
107 | name: 'activate_scheduled_absolute',
108 | },
109 | ]}
110 | translateChoice={false}
111 | />
112 |
113 | {({ formData, ...rest }) => {
114 | switch (get(formData, '$staged.activation.mode')) {
115 | case 'activate_scheduled_relative':
116 | return (
117 |
122 | );
123 | case 'activate_scheduled_absolute':
124 | return (
125 |
130 | );
131 | default:
132 | set(
133 | formData,
134 | '$staged.activation.requested_time',
135 | null
136 | );
137 | return null;
138 | }
139 | }}
140 |
141 |
142 |
149 |
150 |
151 | );
152 |
153 | export default ReceiversEdit;
154 |
--------------------------------------------------------------------------------
/Development/src/pages/receivers/ReceiversList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import ActiveField from '../../components/ActiveField';
13 | import FilterPanel, {
14 | AutocompleteFilter,
15 | BooleanFilter,
16 | StringFilter,
17 | } from '../../components/FilterPanel';
18 | import {
19 | FORMATS,
20 | ParameterField,
21 | TRANSPORTS,
22 | parameterAutocompleteProps,
23 | } from '../../components/ParameterRegisters';
24 | import PaginationButtons from '../../components/PaginationButtons';
25 | import ListActions from '../../components/ListActions';
26 | import useGetList from '../../components/useGetList';
27 | import { queryVersion, useJSONSetting } from '../../settings';
28 |
29 | const ReceiversList = props => {
30 | const [filter, setFilter] = useJSONSetting('Receivers Filter');
31 | const [paginationURL, setPaginationURL] = useState(null);
32 | const { data, loaded, pagination, url } = useGetList({
33 | ...props,
34 | filter,
35 | paginationURL,
36 | });
37 | if (!loaded) return ;
38 |
39 | const nextPage = label => {
40 | setPaginationURL(pagination[label]);
41 | };
42 |
43 | return (
44 | <>
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
59 |
63 | {queryVersion() >= 'v1.2' && (
64 |
68 | )}
69 |
70 |
71 |
72 |
73 |
74 |
79 | Label
80 |
81 | Format
82 | Transport
83 | {queryVersion() >= 'v1.2' && (
84 | Active
85 | )}
86 |
87 |
88 |
89 | {data.map(item => (
90 |
91 |
92 |
100 |
101 |
102 |
107 |
108 |
109 |
114 |
115 | {queryVersion() >= 'v1.2' && (
116 |
117 |
121 |
122 | )}
123 |
124 | ))}
125 |
126 |
127 |
128 |
133 |
134 |
135 | >
136 | );
137 | };
138 |
139 | export default ReceiversList;
140 |
--------------------------------------------------------------------------------
/Development/src/pages/receivers/index.js:
--------------------------------------------------------------------------------
1 | import ReceiversEdit from './ReceiversEdit';
2 | import ReceiversList from './ReceiversList';
3 | import ReceiversShow from './ReceiversShow';
4 |
5 | export { ReceiversEdit, ReceiversList, ReceiversShow };
6 |
--------------------------------------------------------------------------------
/Development/src/pages/senders/SendersEdit.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Link, Route } from 'react-router-dom';
3 | import { Paper, Tab, Tabs } from '@material-ui/core';
4 | import {
5 | BooleanInput,
6 | Edit,
7 | FormDataConsumer,
8 | SelectInput,
9 | SimpleForm,
10 | TextInput,
11 | } from 'react-admin';
12 | import get from 'lodash/get';
13 | import set from 'lodash/set';
14 | import { useTheme } from '@material-ui/styles';
15 | import ClearIcon from '@material-ui/icons/Clear';
16 | import ConnectionEditActions from '../../components/ConnectionEditActions';
17 | import ConnectionEditToolbar from '../../components/ConnectionEditToolbar';
18 | import ResourceTitle from '../../components/ResourceTitle';
19 | import SenderTransportParamsCardsGrid from './SenderTransportParams';
20 |
21 | const SendersEdit = props => {
22 | const theme = useTheme();
23 | const tabBackgroundColor =
24 | theme.palette.type === 'light'
25 | ? theme.palette.grey[100]
26 | : theme.palette.grey[900];
27 | return (
28 | <>
29 |
30 |
36 |
41 |
46 |
51 |
57 |
62 |
63 |
64 |
65 |
66 |
67 | }
71 | />
72 | >
73 | );
74 | };
75 |
76 | const EditStagedTab = props => (
77 | }
81 | actions={}
82 | >
83 | }
85 | redirect={`/senders/${props.id}/show/staged`}
86 | >
87 |
88 |
92 | },
97 | {
98 | id: 'activate_immediate',
99 | name: 'activate_immediate',
100 | },
101 | {
102 | id: 'activate_scheduled_relative',
103 | name: 'activate_scheduled_relative',
104 | },
105 | {
106 | id: 'activate_scheduled_absolute',
107 | name: 'activate_scheduled_absolute',
108 | },
109 | ]}
110 | translateChoice={false}
111 | />
112 |
113 | {({ formData, ...rest }) => {
114 | switch (get(formData, '$staged.activation.mode')) {
115 | case 'activate_scheduled_relative':
116 | return (
117 |
122 | );
123 | case 'activate_scheduled_absolute':
124 | return (
125 |
130 | );
131 | default:
132 | set(
133 | formData,
134 | '$staged.activation.requested_time',
135 | null
136 | );
137 | return null;
138 | }
139 | }}
140 |
141 |
142 |
143 |
144 | );
145 |
146 | export default SendersEdit;
147 |
--------------------------------------------------------------------------------
/Development/src/pages/senders/SendersList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import ActiveField from '../../components/ActiveField';
13 | import FilterPanel, {
14 | AutocompleteFilter,
15 | BooleanFilter,
16 | StringFilter,
17 | } from '../../components/FilterPanel';
18 | import {
19 | ParameterField,
20 | TRANSPORTS,
21 | parameterAutocompleteProps,
22 | } from '../../components/ParameterRegisters';
23 | import PaginationButtons from '../../components/PaginationButtons';
24 | import ListActions from '../../components/ListActions';
25 | import useGetList from '../../components/useGetList';
26 | import { queryVersion, useJSONSetting } from '../../settings';
27 |
28 | const SendersList = props => {
29 | const [filter, setFilter] = useJSONSetting('Senders Filter');
30 | const [paginationURL, setPaginationURL] = useState(null);
31 | const { data, loaded, pagination, url } = useGetList({
32 | ...props,
33 | filter,
34 | paginationURL,
35 | });
36 | if (!loaded) return ;
37 |
38 | const nextPage = label => {
39 | setPaginationURL(pagination[label]);
40 | };
41 |
42 | return (
43 | <>
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
58 | {queryVersion() >= 'v1.2' && (
59 |
63 | )}
64 |
65 |
66 |
67 |
68 |
69 |
74 | Label
75 |
76 | Transport
77 | {queryVersion() >= 'v1.2' && (
78 | Active
79 | )}
80 |
81 |
82 |
83 | {data.map(item => (
84 |
85 |
86 |
94 |
95 |
96 |
101 |
102 | {queryVersion() >= 'v1.2' && (
103 |
104 |
108 |
109 | )}
110 |
111 | ))}
112 |
113 |
114 |
115 |
120 |
121 |
122 | >
123 | );
124 | };
125 |
126 | export default SendersList;
127 |
--------------------------------------------------------------------------------
/Development/src/pages/senders/index.js:
--------------------------------------------------------------------------------
1 | import SendersEdit from './SendersEdit';
2 | import SendersList from './SendersList';
3 | import SendersShow from './SendersShow';
4 |
5 | export { SendersEdit, SendersList, SendersShow };
6 |
--------------------------------------------------------------------------------
/Development/src/pages/sources/SourcesList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import FilterPanel, {
13 | AutocompleteFilter,
14 | RateFilter,
15 | StringFilter,
16 | } from '../../components/FilterPanel';
17 | import {
18 | FORMATS,
19 | ParameterField,
20 | parameterAutocompleteProps,
21 | } from '../../components/ParameterRegisters';
22 | import PaginationButtons from '../../components/PaginationButtons';
23 | import ListActions from '../../components/ListActions';
24 | import useGetList from '../../components/useGetList';
25 | import { queryVersion, useJSONSetting } from '../../settings';
26 |
27 | const SourcesList = props => {
28 | const [filter, setFilter] = useJSONSetting('Sources Filter');
29 | const [paginationURL, setPaginationURL] = useState(null);
30 | const { data, loaded, pagination, url } = useGetList({
31 | ...props,
32 | filter,
33 | paginationURL,
34 | });
35 | if (!loaded) return ;
36 |
37 | const nextPage = label => {
38 | setPaginationURL(pagination[label]);
39 | };
40 |
41 | return (
42 | <>
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {queryVersion() >= 'v1.1' && (
54 |
55 | )}
56 |
60 | {queryVersion() >= 'v1.3' && (
61 |
66 | )}
67 |
68 |
69 |
70 |
71 |
72 |
77 | Label
78 |
79 | Format
80 | {queryVersion() >= 'v1.3' && (
81 | Event Type
82 | )}
83 |
84 |
85 |
86 | {data.map(item => (
87 |
88 |
89 |
97 |
98 |
99 |
104 |
105 | {queryVersion() >= 'v1.3' && (
106 | {item.event_type}
107 | )}
108 |
109 | ))}
110 |
111 |
112 |
113 |
118 |
119 |
120 | >
121 | );
122 | };
123 |
124 | const eventTypes = ['boolean', 'string', 'number'];
125 |
126 | export default SourcesList;
127 |
--------------------------------------------------------------------------------
/Development/src/pages/sources/SourcesShow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ArrayField,
4 | ListButton,
5 | ReferenceArrayField,
6 | ReferenceField,
7 | ReferenceManyField,
8 | ShowContextProvider,
9 | ShowView,
10 | SimpleShowLayout,
11 | SingleFieldList,
12 | TextField,
13 | TopToolbar,
14 | useRecordContext,
15 | useShowController,
16 | } from 'react-admin';
17 | import get from 'lodash/get';
18 | import LinkChipField from '../../components/LinkChipField';
19 | import ObjectField from '../../components/ObjectField';
20 | import { FORMATS, ParameterField } from '../../components/ParameterRegisters';
21 | import RateField from '../../components/RateField';
22 | import RawButton from '../../components/RawButton';
23 | import ResourceTitle from '../../components/ResourceTitle';
24 | import SanitizedDivider from '../../components/SanitizedDivider';
25 | import TAIField from '../../components/TAIField';
26 | import UnsortableDatagrid from '../../components/UnsortableDatagrid';
27 | import { queryVersion } from '../../settings';
28 |
29 | const SourcesShowActions = ({ basePath, data, resource }) => (
30 | }>
31 | {data ? : null}
32 |
33 |
34 | );
35 |
36 | export const SourcesShow = props => {
37 | const controllerProps = useShowController(props);
38 | return (
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | const SourcesShowView = props => {
46 | const { record } = useRecordContext();
47 | return (
48 | }
51 | actions={}
52 | >
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {queryVersion() >= 'v1.1' && (
61 |
62 | )}
63 | {queryVersion() >= 'v1.1' && (
64 |
65 | )}
66 |
67 | {queryVersion() >= 'v1.1' &&
68 | get(record, 'format') === 'urn:x-nmos:format:audio' && (
69 |
70 |
71 |
72 |
73 |
74 |
75 | )}
76 | {queryVersion() >= 'v1.3' &&
77 | get(record, 'format') === 'urn:x-nmos:format:data' && (
78 |
79 | )}
80 |
81 |
86 |
87 |
88 |
89 |
90 |
96 |
97 |
98 |
99 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default SourcesShow;
114 |
--------------------------------------------------------------------------------
/Development/src/pages/sources/index.js:
--------------------------------------------------------------------------------
1 | import SourcesList from './SourcesList';
2 | import SourcesShow from './SourcesShow';
3 |
4 | export { SourcesList, SourcesShow };
5 |
--------------------------------------------------------------------------------
/Development/src/pages/subscriptions/SubscriptionsCreate.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | BooleanInput,
4 | Create,
5 | ListButton,
6 | NumberInput,
7 | SelectInput,
8 | SimpleForm,
9 | Toolbar,
10 | TopToolbar,
11 | } from 'react-admin';
12 | import ObjectInput from '../../components/ObjectInput';
13 | import RawButton from '../../components/RawButton';
14 |
15 | const SubscriptionsCreateActions = ({ basePath, data, resource }) => (
16 |
17 | {data ? : null}
18 |
19 |
20 | );
21 |
22 | const SubscriptionsCreate = props => (
23 | } {...props}>
24 | }
26 | redirect="show"
27 | >
28 | value}
42 | />
43 |
48 |
49 |
50 |
51 |
52 | );
53 |
54 | export default SubscriptionsCreate;
55 |
--------------------------------------------------------------------------------
/Development/src/pages/subscriptions/SubscriptionsList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { BooleanField, Loading, ShowButton, Title } from 'react-admin';
12 | import get from 'lodash/get';
13 | import DeleteButton from '../../components/DeleteButton';
14 | import FilterPanel, {
15 | AutocompleteFilter,
16 | BooleanFilter,
17 | NumberFilter,
18 | StringFilter,
19 | } from '../../components/FilterPanel';
20 | import PaginationButtons from '../../components/PaginationButtons';
21 | import ListActions from '../../components/ListActions';
22 | import useGetList from '../../components/useGetList';
23 | import { useJSONSetting } from '../../settings';
24 |
25 | const SubscriptionsList = props => {
26 | const [filter, setFilter] = useJSONSetting('Subscriptions Filter');
27 | const [paginationURL, setPaginationURL] = useState(null);
28 | const { data, loaded, pagination, url } = useGetList({
29 | ...props,
30 | filter,
31 | paginationURL,
32 | });
33 | if (!loaded) return ;
34 |
35 | const nextPage = label => {
36 | setPaginationURL(pagination[label]);
37 | };
38 |
39 | return (
40 | <>
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
55 |
63 |
64 |
65 |
66 |
67 |
68 |
73 | Resource Path
74 |
75 | Persist
76 | Max Update Rate (ms)
77 |
78 |
79 |
80 |
81 | {data.map(item => (
82 |
83 |
84 |
92 |
93 |
94 |
98 |
99 |
100 | {item.max_update_rate_ms}
101 |
102 |
103 | {get(item, 'persist') && (
104 |
109 | )}
110 |
111 |
112 | ))}
113 |
114 |
115 |
120 |
121 |
122 | >
123 | );
124 | };
125 |
126 | const resourcePaths = [
127 | '/nodes',
128 | '/devices',
129 | '/sources',
130 | '/flows',
131 | '/senders',
132 | '/receivers',
133 | ];
134 |
135 | export default SubscriptionsList;
136 |
--------------------------------------------------------------------------------
/Development/src/pages/subscriptions/SubscriptionsShow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | BooleanField,
4 | ShowContextProvider,
5 | ShowView,
6 | SimpleShowLayout,
7 | TextField,
8 | Toolbar,
9 | useRecordContext,
10 | useShowController,
11 | } from 'react-admin';
12 | import get from 'lodash/get';
13 | import DeleteButton from '../../components/DeleteButton';
14 | import ObjectField from '../../components/ObjectField';
15 | import ResourceShowActions from '../../components/ResourceShowActions';
16 | import ResourceTitle from '../../components/ResourceTitle';
17 | import SanitizedDivider from '../../components/SanitizedDivider';
18 | import UrlField from '../../components/URLField';
19 | import { queryVersion } from '../../settings';
20 |
21 | export const SubscriptionsShow = props => {
22 | const controllerProps = useShowController(props);
23 | return (
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | const SubscriptionsShowView = props => {
31 | const { record } = useRecordContext();
32 | return (
33 | <>
34 | }
37 | actions={}
38 | >
39 |
40 |
41 |
42 |
43 |
47 |
48 |
49 |
50 |
51 | {queryVersion() >= 'v1.3' && (
52 |
53 | )}
54 |
55 |
56 | {get(record, 'id') && (
57 | // Toolbar will override the DeleteButton resource prop
58 |
59 |
60 |
61 | )}
62 | >
63 | );
64 | };
65 |
66 | export default SubscriptionsShow;
67 |
--------------------------------------------------------------------------------
/Development/src/pages/subscriptions/index.js:
--------------------------------------------------------------------------------
1 | import SubscriptionsCreate from './SubscriptionsCreate';
2 | import SubscriptionsList from './SubscriptionsList';
3 | import SubscriptionsShow from './SubscriptionsShow';
4 |
5 | export { SubscriptionsCreate, SubscriptionsList, SubscriptionsShow };
6 |
--------------------------------------------------------------------------------
/Development/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log(
69 | 'New content is available; please refresh.'
70 | );
71 | } else {
72 | // At this point, everything has been precached.
73 | // It's the perfect time to display a
74 | // "Content is cached for offline use." message.
75 | console.log('Content is cached for offline use.');
76 | }
77 | }
78 | };
79 | };
80 | })
81 | .catch(error => {
82 | console.error('Error during service worker registration:', error);
83 | });
84 | }
85 |
86 | function checkValidServiceWorker(swUrl) {
87 | // Check if the service worker can be found. If it can't reload the page.
88 | fetch(swUrl)
89 | .then(response => {
90 | // Ensure service worker exists, and that we really are getting a JS file.
91 | if (
92 | response.status === 404 ||
93 | response.headers.get('content-type').indexOf('javascript') ===
94 | -1
95 | ) {
96 | // No service worker found. Probably a different app. Reload the page.
97 | navigator.serviceWorker.ready.then(registration => {
98 | registration.unregister().then(() => {
99 | window.location.reload();
100 | });
101 | });
102 | } else {
103 | // Service worker found. Proceed as normal.
104 | registerValidSW(swUrl);
105 | }
106 | })
107 | .catch(() => {
108 | console.log(
109 | 'No internet connection found. App is running in offline mode.'
110 | );
111 | });
112 | }
113 |
114 | export function unregister() {
115 | if ('serviceWorker' in navigator) {
116 | navigator.serviceWorker.ready.then(registration => {
117 | registration.unregister();
118 | });
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Development/src/settings.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from 'react';
2 | import { get, isEqual } from 'lodash';
3 | import CONFIG from './config.json';
4 |
5 | export const LOGGING_API = 'Logging API';
6 | export const QUERY_API = 'Query API';
7 | export const DNSSD_API = 'DNS-SD API';
8 |
9 | export const USE_RQL = 'RQL';
10 | export const PAGING_LIMIT = 'Paging Limit';
11 |
12 | export const FRIENDLY_PARAMETERS = 'Friendly Parameters';
13 |
14 | export const disabledSetting = name => get(CONFIG, `${name}.disabled`);
15 | export const hiddenSetting = name => get(CONFIG, `${name}.hidden`);
16 |
17 | export const concatUrl = (url, path) => {
18 | return (
19 | url + (url.endsWith('/') && path.startsWith('/') ? path.slice(1) : path)
20 | );
21 | };
22 |
23 | const defaultUrl = api => {
24 | const configUrl = get(CONFIG, `${api}.value`);
25 | if (configUrl) {
26 | return configUrl;
27 | }
28 | let baseUrl = window.location.protocol + '//' + window.location.host;
29 | switch (api) {
30 | case LOGGING_API:
31 | return baseUrl + '/log/v1.0';
32 | case QUERY_API:
33 | return baseUrl + '/x-nmos/query/v1.3';
34 | case DNSSD_API:
35 | return baseUrl + '/x-dns-sd/v1.1';
36 | default:
37 | // not expected to be used
38 | return '';
39 | }
40 | };
41 |
42 | export const apiUrl = api => {
43 | if (disabledSetting(api)) {
44 | return defaultUrl(api);
45 | }
46 | return window.localStorage.getItem(api) || defaultUrl(api);
47 | };
48 | // deprecated, see useSettingsContext()
49 | export const setApiUrl = (api, url) => {
50 | if (disabledSetting(api)) {
51 | console.error(`Configuration does not allow ${api} to be changed`);
52 | return;
53 | }
54 | if (url && url !== defaultUrl(api)) {
55 | window.localStorage.setItem(api, url);
56 | } else {
57 | window.localStorage.removeItem(api);
58 | }
59 | };
60 |
61 | // version, e.g. 'v1.3', is always the last path component
62 | export const apiVersion = api => apiUrl(api).match(/([^/]+)\/?$/g)[0];
63 |
64 | export const queryVersion = () => apiVersion(QUERY_API);
65 |
66 | // single value, not per-API, right now
67 | // default to 10 rather than leaving undefined and letting the API use its default,
68 | // in order to simplify pagination with client-side filtered results
69 | export const apiPagingLimit = api => getJSONSetting(PAGING_LIMIT, 10);
70 | // deprecated, see useSettingsContext()
71 | export const setApiPagingLimit = (api, pagingLimit) => {
72 | if (typeof pagingLimit === 'number') {
73 | setJSONSetting(PAGING_LIMIT, pagingLimit);
74 | } else {
75 | unsetJSONSetting(PAGING_LIMIT);
76 | }
77 | };
78 |
79 | // single value, not per-API, right now
80 | export const apiUsingRql = api => getJSONSetting(USE_RQL, true);
81 | // deprecated, see useSettingsContext()
82 | export const setApiUsingRql = (api, rql) => {
83 | if (typeof rql === 'boolean') {
84 | setJSONSetting(USE_RQL, rql);
85 | } else {
86 | unsetJSONSetting(USE_RQL);
87 | }
88 | };
89 |
90 | export const getJSONSetting = (name, defaultValue = {}) => {
91 | const configValue = get(CONFIG, `${name}.value`);
92 | if (configValue !== undefined) {
93 | defaultValue = configValue;
94 | }
95 | if (disabledSetting(name)) {
96 | return defaultValue;
97 | }
98 | try {
99 | const stored = window.localStorage.getItem(name);
100 | // treat empty string same as null (not found)
101 | return stored ? JSON.parse(stored) : defaultValue;
102 | } catch (e) {
103 | // treat parse error same as not found
104 | return defaultValue;
105 | }
106 | };
107 |
108 | export const setJSONSetting = (name, value) => {
109 | if (disabledSetting(name)) {
110 | console.error(`Configuration does not allow ${name} to be changed`);
111 | return;
112 | }
113 | // note that e.g. NaN becomes null
114 | const stored = JSON.stringify(value);
115 | window.localStorage.setItem(name, stored);
116 | };
117 |
118 | export const unsetJSONSetting = name => {
119 | window.localStorage.removeItem(name);
120 | };
121 |
122 | export const useJSONSetting = (name, defaultValue = {}) => {
123 | const [setting, setSetting] = useState(getJSONSetting(name, defaultValue));
124 | useEffect(() => {
125 | const configValue = get(CONFIG, `${name}.value`);
126 | const hasConfigValue = configValue !== undefined;
127 | if (!isEqual(setting, hasConfigValue ? configValue : defaultValue)) {
128 | setJSONSetting(name, setting);
129 | } else {
130 | unsetJSONSetting(name);
131 | }
132 | }, [name, setting, defaultValue]);
133 |
134 | return [setting, setSetting];
135 | };
136 |
137 | const useSettings = () => {
138 | const [values, setValues] = useState({
139 | [QUERY_API]: apiUrl(QUERY_API),
140 | [LOGGING_API]: apiUrl(LOGGING_API),
141 | [DNSSD_API]: apiUrl(DNSSD_API),
142 | [PAGING_LIMIT]: apiPagingLimit(QUERY_API),
143 | [USE_RQL]: apiUsingRql(QUERY_API),
144 | [FRIENDLY_PARAMETERS]: getJSONSetting(FRIENDLY_PARAMETERS, false),
145 | });
146 |
147 | const isEffective = name => !hiddenSetting(name) && !disabledSetting(name);
148 | useEffect(() => {
149 | if (isEffective(QUERY_API)) setApiUrl(QUERY_API, values[QUERY_API]);
150 | if (isEffective(LOGGING_API))
151 | setApiUrl(LOGGING_API, values[LOGGING_API]);
152 | if (isEffective(DNSSD_API)) setApiUrl(DNSSD_API, values[DNSSD_API]);
153 | if (isEffective(PAGING_LIMIT))
154 | setApiPagingLimit(QUERY_API, values[PAGING_LIMIT]);
155 | if (isEffective(USE_RQL)) setApiUsingRql(QUERY_API, values[USE_RQL]);
156 | if (isEffective(FRIENDLY_PARAMETERS))
157 | setJSONSetting(FRIENDLY_PARAMETERS, values[FRIENDLY_PARAMETERS]);
158 | }, [values]);
159 |
160 | return [values, setValues];
161 | };
162 |
163 | const SettingsContext = createContext();
164 |
165 | export const SettingsContextProvider = props => {
166 | const [values, setValues] = useSettings();
167 | return ;
168 | };
169 |
170 | export const useSettingsContext = () => useContext(SettingsContext);
171 |
--------------------------------------------------------------------------------
/Development/src/theme/ThemeContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ThemeProvider } from '@material-ui/styles';
3 | import { createMuiTheme, responsiveFontSizes } from '@material-ui/core/styles';
4 | import { get } from 'lodash';
5 | import CONFIG from '../config.json';
6 | import { disabledSetting, useJSONSetting } from '../settings';
7 |
8 | export const ThemeContext = React.createContext({
9 | theme: 'light',
10 | toggleTheme: () => {},
11 | });
12 |
13 | export const AppThemeProvider = ({ children }) => {
14 | const themePalette = get(CONFIG, 'palette', {
15 | primary: {
16 | main: 'rgb(45,117,199)',
17 | contrastText: '#fff',
18 | },
19 | secondary: {
20 | main: 'rgb(0,47,103)',
21 | contrastText: '#fff',
22 | },
23 | });
24 | const [themeState, setThemeState] = useJSONSetting('theme', {
25 | type: 'light',
26 | });
27 |
28 | const theme = responsiveFontSizes(
29 | createMuiTheme({
30 | palette: {
31 | ...themePalette,
32 | type: themeState.type,
33 | },
34 | sidebar: {
35 | width: 240,
36 | closedWidth: 72,
37 | },
38 | overrides: {
39 | MuiTableCell: {
40 | sizeSmall: {
41 | '&:last-child': {
42 | paddingRight: null,
43 | },
44 | },
45 | },
46 | RaReferenceField: {
47 | link: {
48 | color: null,
49 | textDecoration: null,
50 | },
51 | },
52 | },
53 | })
54 | );
55 |
56 | const toggleTheme = () => {
57 | if (disabledSetting('theme')) return;
58 | const toggledType = themeState.type === 'light' ? 'dark' : 'light';
59 | setThemeState({ type: toggledType });
60 | };
61 |
62 | return (
63 |
64 |
65 | {React.cloneElement(children, { theme })}
66 |
67 |
68 | );
69 | };
70 |
71 | export default ThemeContext;
72 |
--------------------------------------------------------------------------------
/Documents/Dependencies.md:
--------------------------------------------------------------------------------
1 | # Dependencies
2 |
3 | The codebase utilizes a number of great open-source projects (licenses vary).
4 |
5 | - [React](https://reactjs.org)
6 | - [react-admin](https://github.com/marmelab/react-admin)
7 |
8 | ## Preparation
9 |
10 | Install the [yarn](https://yarnpkg.com) package manager for your platform.
11 |
12 | Then, to install the required packages, run `yarn install` in your terminal from the [Development](../Development) directory.
13 |
14 | # What Next?
15 |
16 | These [instructions](Getting-Started.md) explain how to build and deploy the nmos-js client itself.
17 |
--------------------------------------------------------------------------------
/Documents/Getting-Started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | The following instructions describe how to build and deploy this software.
4 |
5 | ## Preparation
6 |
7 | Set up the [external dependencies](Dependencies.md#preparation) before proceeding.
8 |
9 | Once packages are installed, run `yarn start` to launch the client on a development server at http://localhost:3000.
10 |
11 | To build for release, simply run `yarn build`. The output will be written to a build directory. Instructions on how to serve the build will be provided at this point.
12 |
13 | ESLint and Prettier can be run using `yarn lint`. If you wish to list the warnings without writing changes `yarn lint-check` can be used instead.
14 |
15 | ## Run as a Docker container
16 |
17 | At this point in time, a prebuilt container is not provided. Luckily it is easy to build it from source!
18 | You will need to have git installed as well as [Docker](https://docs.docker.com/install/).
19 |
20 | ```bash
21 | git clone https://github.com/sony/nmos-js
22 | cd nmos-js
23 | docker build -t nmos-js Development
24 | ```
25 |
26 | Start the Docker container by binding nmos-js to an external port.
27 |
28 | ```bash
29 | docker run -d --name=nmos-js -p 80:80 nmos-js
30 | ```
31 |
--------------------------------------------------------------------------------
/Documents/Repository-Structure.md:
--------------------------------------------------------------------------------
1 | # Repository Structure
2 |
3 | - [Development](../Development)
4 | Source code for the software
5 | - [Documents](../Documents)
6 | Documentation
7 |
--------------------------------------------------------------------------------
/Documents/images/jt-nm-tested-03-20-controller.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sony/nmos-js/5896ef98dfa236eb76c2521f33b699a14450d85d/Documents/images/jt-nm-tested-03-20-controller.png
--------------------------------------------------------------------------------
/Documents/images/sea-lion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sony/nmos-js/5896ef98dfa236eb76c2521f33b699a14450d85d/Documents/images/sea-lion.png
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | nmos-js: An NMOS Client in JavaScript
2 | Copyright (c) 2017 Sony Corporation. All Rights Reserved.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # An NMOS Client in JavaScript [][build-test]
2 | [build-test]: https://github.com/sony/nmos-js/actions?query=workflow%3Abuild-test
3 |
4 | ## Introduction
5 |
6 | This repository contains a client implementation of the [AMWA Networked Media Open Specifications](https://github.com/AMWA-TV/nmos) in JavaScript, [licensed](LICENSE) under the terms of the Apache License 2.0.
7 |
8 | - [AMWA IS-04 NMOS Discovery and Registration Specification](https://amwa-tv.github.io/nmos-discovery-registration)
9 | - [AMWA IS-05 NMOS Device Connection Management Specification](https://amwa-tv.github.io/nmos-device-connection-management)
10 | - [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://specs.amwa.tv/is-08/) (read-only for now)
11 | - [AMWA BCP-004-01 NMOS Receiver Capabilities](https://specs.amwa.tv/bcp-004-01/)
12 |
13 | For more information about AMWA, NMOS and the Networked Media Incubator, please refer to http://amwa.tv/.
14 |
15 | The [repository structure](Documents/Repository-Structure.md), and the [external dependencies](Documents/Dependencies.md), are outlined in the documentation.
16 |
17 | ### Getting Started With NMOS
18 |
19 | The [Easy-NMOS](https://github.com/rhastie/easy-nmos) starter kit allows the user to launch a simple NMOS setup with minimal installation steps.
20 | It relies on nmos-js to provide an NMOS Client that works with an NMOS Registry and a virtual NMOS Node in a Docker Compose network, along with the AMWA NMOS Testing Tool and supporting services.
21 |
22 | ### Getting Started For Developers
23 |
24 | Easy-NMOS is also a great first way to explore the relationship between NMOS services.
25 |
26 | The nmos-js codebase is intended to work with any NMOS Registry, but can take advantage of the features of the [nmos-cpp](https://github.com/sony/nmos-cpp) implementation.
27 |
28 | After setting up the dependencies, follow these [instructions](Documents/Getting-Started.md) to build and deploy the nmos-js client itself.
29 |
30 | The web application can also be packaged and deployed with the [nmos-cpp-registry application](https://github.com/sony/nmos-cpp).
31 | Copy the contents of the nmos-js build directory into the admin directory next to the nmos-cpp-registry executable.
32 |
33 | ## Agile Development
34 |
35 | [
](https://jt-nm.org/jt-nm_tested/)
36 |
37 | The nmos-js client, like the NMOS Specifications, is intended to be always ready, but steadily developing.
38 | The nmos-js client works as both an NMOS Registry browser, using the IS-04 Query API, and provides connection management, using the IS-05 Connection API.
39 | When used with the nmos-cpp-registry, it also provides access to registry log messages.
40 | It has been successfully tested in many AMWA Networked Media Incubator workshops, and in the [JT-NM Tested](https://jt-nm.org/jt-nm_tested/) programme.
41 |
42 | ### Recent Activity
43 |
44 | The implementation is designed to be extended. Development is ongoing, following the evolution of the NMOS specifications in the AMWA Networked Media Incubator.
45 |
46 | Recent activity on the project (newest first):
47 |
48 | - Read-only support for IS-08 Audio Channel Mapping
49 | - Support for BCP-004-01 Receiver Capabilities
50 | - JT-NM Tested 03/20 badge (packaged and deployed on a Mellanox SN2010 Switch)
51 | - Improved and simplified connection management
52 | - Periodic refresh
53 | - Lots of other incremental improvements
54 | - Added support for multi-homed Nodes
55 | - Added connection management support for RTP, WebSocket and MQTT transports
56 | - Added a dark theme
57 | - Switched to [react-admin](https://github.com/marmelab/react-admin) framework from [ng-admin](https://github.com/marmelab/ng-admin)
58 |
59 | ## Contributing
60 |
61 | We welcome bug reports, feature requests and contributions to the implementation and documentation.
62 | Please have a look at the simple [Contribution Guidelines](CONTRIBUTING.md).
63 |
64 | Thank you for your interest!
65 |
66 | 
67 |
--------------------------------------------------------------------------------