├── .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 | }) => Logo; 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 |