From 7953b008a8b9c6d68c33ca88de29d85f23ca380c Mon Sep 17 00:00:00 2001 From: ygzaydn <32926423+ygzaydn@users.noreply.github.com> Date: Wed, 23 Sep 2026 05:21:37 +0300 Subject: [PATCH] [EIR] add WebUI equipment identity management (#4795) * feat(eir): add WebUI equipment identity management * fix(eir): align WebUI rule validation and sample test setup * fix(eir): keep EIR disabled in sample configuration --- tests/app/app-init.c | 6 + webui/server/models/eir.js | 19 +++ webui/server/routes/db.js | 3 + webui/src/components/Base/Sidebar.js | 5 + webui/src/components/Eir/Edit.js | 74 ++++++++++ webui/src/components/Eir/Item.js | 115 +++++++++++++++ webui/src/components/Eir/List.js | 47 ++++++ webui/src/components/Eir/Search.js | 52 +++++++ webui/src/components/Eir/View.js | 190 +++++++++++++++++++++++++ webui/src/components/Eir/index.js | 5 + webui/src/components/index.js | 4 +- webui/src/containers/App.js | 6 +- webui/src/containers/Eir/Collection.js | 148 +++++++++++++++++++ webui/src/containers/Eir/Document.js | 126 ++++++++++++++++ webui/src/containers/Eir/index.js | 3 + webui/src/modules/crud/eir.js | 19 +++ 16 files changed, 820 insertions(+), 2 deletions(-) create mode 100644 webui/server/models/eir.js create mode 100644 webui/src/components/Eir/Edit.js create mode 100644 webui/src/components/Eir/Item.js create mode 100644 webui/src/components/Eir/List.js create mode 100644 webui/src/components/Eir/Search.js create mode 100644 webui/src/components/Eir/View.js create mode 100644 webui/src/components/Eir/index.js create mode 100644 webui/src/containers/Eir/Collection.js create mode 100644 webui/src/containers/Eir/Document.js create mode 100644 webui/src/containers/Eir/index.js create mode 100644 webui/src/modules/crud/eir.js diff --git a/tests/app/app-init.c b/tests/app/app-init.c index 0139c5c29..72c8607f7 100644 --- a/tests/app/app-init.c +++ b/tests/app/app-init.c @@ -36,6 +36,7 @@ static ogs_thread_t *pcf_thread = NULL; static ogs_thread_t *nssf_thread = NULL; static ogs_thread_t *bsf_thread = NULL; static ogs_thread_t *udr_thread = NULL; +static ogs_thread_t *eir_thread = NULL; int app_initialize(const char *const argv[]) { @@ -96,6 +97,10 @@ int app_initialize(const char *const argv[]) if (ogs_global_conf()->parameter.no_udr == 0) udr_thread = test_child_create("udr", 0, argv_out); + if (ogs_global_conf()->parameter.no_eir == 0 && + ogs_global_conf()->parameter.eir_count > 0) + eir_thread = test_child_create("eir", 0, argv_out); + /* * Wait for all sockets listening * @@ -118,6 +123,7 @@ void app_terminate(void) if (upf_thread) ogs_thread_destroy(upf_thread); if (udr_thread) ogs_thread_destroy(udr_thread); + if (eir_thread) ogs_thread_destroy(eir_thread); if (bsf_thread) ogs_thread_destroy(bsf_thread); if (nssf_thread) ogs_thread_destroy(nssf_thread); if (pcf_thread) ogs_thread_destroy(pcf_thread); diff --git a/webui/server/models/eir.js b/webui/server/models/eir.js new file mode 100644 index 000000000..26152183d --- /dev/null +++ b/webui/server/models/eir.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; +const Eir = new Schema( + { + pei: { + $type: String, + required: true, + match: /^(imei-[0-9]{15}|imeisv-[0-9]{16})$/ + }, + supi: { $type: String, default: undefined, match: /^imsi-[0-9]{6,15}$/ }, + status: { + $type: String, + required: true, + enum: ['WHITELISTED', 'BLACKLISTED', 'GREYLISTED'] + } + }, + { typeKey: '$type', collection: 'eir' } +); +module.exports = mongoose.model('Eir', Eir); diff --git a/webui/server/routes/db.js b/webui/server/routes/db.js index 751a0f3dc..70570886c 100644 --- a/webui/server/routes/db.js +++ b/webui/server/routes/db.js @@ -10,6 +10,9 @@ restify.serve(router, Subscriber, { idProperty: 'imsi' }); +const Eir = require('../models/eir'); +restify.serve(router, Eir, { prefix: '', version: '' }); + const Profile = require('../models/profile'); restify.serve(router, Profile, { prefix: '', diff --git a/webui/src/components/Base/Sidebar.js b/webui/src/components/Base/Sidebar.js index 521b4bcb3..c8077664a 100644 --- a/webui/src/components/Base/Sidebar.js +++ b/webui/src/components/Base/Sidebar.js @@ -7,6 +7,7 @@ import oc from 'open-color'; import SubscriberIcon from 'react-icons/lib/md/person-add'; import ProfileIcon from 'react-icons/lib/md/content-copy'; import AccountIcon from 'react-icons/lib/md/vpn-key'; +import EirIcon from 'react-icons/lib/md/phonelink-lock'; const Menu = styled.div` display: block; @@ -85,6 +86,10 @@ const Sidebar = ({ isOpen, width, selectedView, onSelectView }) => ( Subscriber + + + EIR + Profile diff --git a/webui/src/components/Eir/Edit.js b/webui/src/components/Eir/Edit.js new file mode 100644 index 000000000..5c6b1ee07 --- /dev/null +++ b/webui/src/components/Eir/Edit.js @@ -0,0 +1,74 @@ +import { Component } from 'react'; +import withWidth, { SMALL } from 'helpers/with-width'; +import { Form } from 'components'; +const schema = { + title: '', + type: 'object', + properties: { + pei_type: { + type: 'string', + title: 'Equipment Identity Type*', + enum: ['imei', 'imeisv'], + enumNames: ['IMEI (15 digits)', 'IMEISV (16 digits)'], + default: 'imei' + }, + pei: { + type: 'string', + title: 'IMEI / IMEISV*', + required: true, + pattern: '^\\d+$', + messages: { pattern: 'Only digits are allowed' } + }, + imsi: { + type: 'string', + title: 'IMSI (optional subscriber rule)', + pattern: '^\\d*$', + messages: { pattern: 'Only digits are allowed' } + }, + status: { + type: 'string', + title: 'Equipment Status*', + required: true, + enum: ['WHITELISTED', 'GREYLISTED', 'BLACKLISTED'], + default: 'WHITELISTED' + } + } +}; +class Edit extends Component { + constructor(props) { + super(props); + this.state = this.stateFrom(props); + } + componentWillReceiveProps(props) { + this.setState(this.stateFrom(props)); + } + stateFrom(props) { + const uiSchema = {}; + if (props.action === 'update') + ['pei_type', 'pei', 'imsi'].forEach( + (key) => (uiSchema[key] = { 'ui:disabled': true }) + ); + else if (props.width !== SMALL) uiSchema.pei = { 'ui:autofocus': true }; + return { formData: props.formData, uiSchema }; + } + render() { + const p = this.props; + return ( +
+ ); + } +} +export default withWidth()(Edit); diff --git a/webui/src/components/Eir/Item.js b/webui/src/components/Eir/Item.js new file mode 100644 index 000000000..008b50e8c --- /dev/null +++ b/webui/src/components/Eir/Item.js @@ -0,0 +1,115 @@ +import { Component } from 'react'; +import styled from 'styled-components'; +import oc from 'open-color'; +import { media } from 'helpers/style-utils'; +import EditIcon from 'react-icons/lib/md/edit'; +import DeleteIcon from 'react-icons/lib/md/delete'; +import { Tooltip, Spinner } from 'components'; + +const Sizer = styled.div` + display: inline-block; + width: 33.3%; + padding: 0.5rem; + ${(p) => p.disabled && 'opacity: 0.5; cursor: not-allowed;'}; + ${media.desktop`width: 50%;`} ${media.tablet`width: 100%;`} +`; +const Card = styled.div` + position: relative; + display: flex; + background: white; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); + cursor: pointer; + ${(p) => p.disabled && 'pointer-events: none;'} + .actions { + position: absolute; + top: 0; + right: 0; + width: 6rem; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + } + &:hover { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); + .actions { + ${(p) => (p.disabled ? 'opacity: 0;' : 'opacity: 1;')}; + } + } +`; +const Pei = styled.div` + padding-left: 1rem; + color: ${oc.gray[8]}; + font-size: 1.25rem; + line-height: 3rem; +`; +const CircleButton = styled.div` + height: 2rem; + width: 2rem; + display: flex; + align-items: center; + justify-content: center; + margin: 1px; + background: white; + color: ${oc.gray[6]}; + border-radius: 1rem; + font-size: 1.5rem; + &:hover { + color: ${oc.indigo[6]}; + } + &.delete:hover { + color: ${oc.pink[6]}; + } +`; +const SpinnerWrapper = styled.div` + position: absolute; + top: 0; + right: 0; + width: 4rem; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +`; +const displayPei = (pei) => (pei || '').replace(/^(imei|imeisv)-/, ''); + +class Item extends Component { + handleEdit = (e) => { + e.stopPropagation(); + this.props.onEdit(this.props.record._id); + }; + handleDelete = (e) => { + e.stopPropagation(); + this.props.onDelete(this.props.record._id); + }; + render() { + const { disabled, record, onView } = this.props; + return ( + + onView(record._id)}> + {displayPei(record.pei)} +
+ + + + + + + + + + +
+ {disabled && ( + + + + )} +
+
+ ); + } +} +export default Item; diff --git a/webui/src/components/Eir/List.js b/webui/src/components/Eir/List.js new file mode 100644 index 000000000..a72211b7d --- /dev/null +++ b/webui/src/components/Eir/List.js @@ -0,0 +1,47 @@ +import styled from 'styled-components'; +import { media, transitions } from 'helpers/style-utils'; +import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'; +import Item from './Item'; +const Wrapper = styled.div` + display: block; + margin: 1rem 0.5rem; + ${media.mobile`margin:.5rem .25rem;`}.eir-enter { + animation: ${transitions.stretchOut} 0.3s ease-in; + animation-fill-mode: forwards; + } + .eir-leave { + animation: ${transitions.shrinkIn} 0.15s ease-in; + animation-fill-mode: forwards; + } +`; +const raw = (value) => (value || '').replace(/^(imei|imeisv|imsi)-/, ''); +export default ({ records, deletedId, onView, onEdit, onDelete, search }) => { + const list = records + .filter( + (record) => + raw(record.pei).indexOf(search) !== -1 || + raw(record.supi).indexOf(search) !== -1 + ) + .sort((a, b) => raw(a.pei).localeCompare(raw(b.pei))) + .map((record) => ( + + )); + return ( + + + {list} + + + ); +}; diff --git a/webui/src/components/Eir/Search.js b/webui/src/components/Eir/Search.js new file mode 100644 index 000000000..873db6389 --- /dev/null +++ b/webui/src/components/Eir/Search.js @@ -0,0 +1,52 @@ +import styled from 'styled-components'; +import oc from 'open-color'; +import { media } from 'helpers/style-utils'; +import SearchIcon from 'react-icons/lib/md/search'; +import ClearIcon from 'react-icons/lib/md/clear'; +const Wrapper = styled.div` + display: flex; + align-items: center; + width: 700px; + margin: 2rem auto 1rem; + background: white; + color: ${oc.gray[6]}; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); + &:hover { + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); + } + ${media.tablet`width:400px;`}${media.mobile`margin:0 auto;width:100%;`} +`; +const Input = styled.input` + padding: 1rem; + margin: 0 auto; + width: 100%; + font-size: 1.5rem; + cursor: text; + border: none; + outline: none; +`; +const Icon = styled.div` + display: inline-flex; + margin-left: 1rem; + font-size: 1.5rem; +`; +const Clear = styled.div` + display: inline-flex; + margin-right: 1rem; + font-size: 1.5rem; + cursor: pointer; +`; +export default ({ value, onChange, onClear }) => ( + + + + + + {value !== '' && ( + + + + )} + +); diff --git a/webui/src/components/Eir/View.js b/webui/src/components/Eir/View.js new file mode 100644 index 000000000..064992a51 --- /dev/null +++ b/webui/src/components/Eir/View.js @@ -0,0 +1,190 @@ +import styled from 'styled-components'; +import oc from 'open-color'; +import { media } from 'helpers/style-utils'; +import EditIcon from 'react-icons/lib/md/edit'; +import DeleteIcon from 'react-icons/lib/md/delete'; +import CloseIcon from 'react-icons/lib/md/close'; +import SecurityIcon from 'react-icons/lib/md/security'; +import PhoneIcon from 'react-icons/lib/md/phone'; +import KeyboardControlIcon from 'react-icons/lib/md/keyboard-control'; +import { Modal, Tooltip } from 'components'; +const Wrapper = styled.div` + display: flex; + flex-direction: column; + position: relative; + width: 700px; + ${media.mobile`width:calc(100vw - 4rem);`}background:white; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); +`; +const Header = styled.div` + position: relative; + display: flex; + background: ${oc.gray[1]}; + .title { + padding: 1.5rem; + color: ${oc.gray[8]}; + font-size: 1.5rem; + } + .actions { + position: absolute; + top: 0; + right: 0; + width: 8rem; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } +`; +const CircleButton = styled.div` + height: 2rem; + width: 2rem; + display: flex; + align-items: center; + justify-content: center; + margin: 1px; + color: ${oc.gray[6]}; + border-radius: 1rem; + font-size: 1.5rem; + &:hover { + color: ${oc.indigo[6]}; + } + &.delete:hover { + color: ${oc.pink[6]}; + } +`; +const Body = styled.div` + display: block; + margin: 0.5rem; + min-height: 190px; + overflow: scroll; +`; +const Details = styled.div` + display: flex; + flex-direction: column; + margin: 0 auto; + color: ${oc.gray[9]}; + .header { + margin: 12px; + font-size: 16px; + } + .body { + display: flex; + flex-direction: row; + flex: 1; + margin: 6px; + } + .left { + width: 80px; + text-align: center; + font-size: 18px; + color: ${oc.gray[6]}; + } + .right { + display: flex; + flex-direction: column; + flex: 1; + } + .data { + flex: 1; + font-size: 12px; + margin: 4px; + } +`; +const raw = (value) => (value || '').replace(/^(imei|imeisv|imsi)-/, ''); +export default ({ + visible, + disableOnClickOutside, + record, + onEdit, + onDelete, + onHide +}) => { + const r = record || {}; + return ( + + +
+
{raw(r.pei)}
+
+ + onEdit(r._id)}> + + + + + onDelete(r._id)}> + + + + + + + + +
+
+ +
+
Equipment Identity Rule
+
+
+ +
+
+
+ {(r.pei || '').split('-')[0].toUpperCase()} + + + Identity Type + +
+
+ {raw(r.pei)} + + + IMEI / IMEISV + +
+ {r.supi && ( +
+ {raw(r.supi)} + + + IMSI + +
+ )} +
+
+
+
+ +
+
+
+ {r.supi ? 'Subscriber Rule' : 'Device Rule'} + + + Rule Type + +
+
+ {r.status} + + + Equipment Status + +
+
+
+
+ +
+
+ ); +}; diff --git a/webui/src/components/Eir/index.js b/webui/src/components/Eir/index.js new file mode 100644 index 000000000..c845bd1ba --- /dev/null +++ b/webui/src/components/Eir/index.js @@ -0,0 +1,5 @@ +import List from './List'; +import Search from './Search'; +import Edit from './Edit'; +import View from './View'; +export { List, Search, Edit, View }; diff --git a/webui/src/components/index.js b/webui/src/components/index.js index a7b438026..bff9ec7b4 100644 --- a/webui/src/components/index.js +++ b/webui/src/components/index.js @@ -19,6 +19,7 @@ import Confirm from './Shared/Confirm'; import * as Subscriber from './Subscriber'; import * as Profile from './Profile'; import * as Account from './Account'; +import * as Eir from './Eir'; export { Layout, @@ -41,5 +42,6 @@ export { Subscriber, Profile, - Account + Account, + Eir } diff --git a/webui/src/containers/App.js b/webui/src/containers/App.js index 4daa7e4bd..fe3beb82b 100644 --- a/webui/src/containers/App.js +++ b/webui/src/containers/App.js @@ -11,6 +11,7 @@ import Notification from 'containers/Notification'; import * as Subscriber from 'containers/Subscriber'; import * as Profile from 'containers/Profile'; import * as Account from 'containers/Account'; +import * as Eir from 'containers/Eir'; class App extends Component { static propTypes = { @@ -36,7 +37,7 @@ class App extends Component { session } = this.props; - if (view === "subscriber") { + if (view === "subscriber" || view === "eir") { document.body.style.backgroundColor = "#e9ecef"; } else { document.body.style.backgroundColor = "white"; @@ -47,6 +48,9 @@ class App extends Component { + + + diff --git a/webui/src/containers/Eir/Collection.js b/webui/src/containers/Eir/Collection.js new file mode 100644 index 000000000..64860befa --- /dev/null +++ b/webui/src/containers/Eir/Collection.js @@ -0,0 +1,148 @@ +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { MODEL, fetchEirRecords, deleteEirRecord } from 'modules/crud/eir'; +import { clearActionStatus } from 'modules/crud/actions'; +import { select, selectActionStatus } from 'modules/crud/selectors'; +import * as Notification from 'modules/notification/actions'; +import { + Layout, + Eir, + Spinner, + FloatingButton, + Blank, + Dimmed, + Confirm +} from 'components'; +import Document from './Document'; +class Collection extends Component { + state = { + search: '', + document: { action: '', visible: false, dimmed: false }, + confirm: { visible: false, id: '' }, + view: { visible: false, disableOnClickOutside: false, id: '' } + }; + componentWillMount() { + if (this.props.records.needsFetch) + this.props.dispatch(this.props.records.fetch); + } + componentWillReceiveProps(nextProps) { + const { records, status, dispatch } = nextProps; + if (records.needsFetch) dispatch(records.fetch); + if (status.response) { + dispatch( + Notification.success({ + title: 'EIR', + message: `${status.id} has been deleted` + }) + ); + dispatch(clearActionStatus(MODEL, 'delete')); + } + } + handleSearchChange = (e) => this.setState({ search: e.target.value }); + handleSearchClear = () => this.setState({ search: '' }); + documentHandler = { + show: (action, payload) => + this.setState({ + document: { action, visible: true, dimmed: true, ...payload }, + view: { ...this.state.view, disableOnClickOutside: true } + }), + hide: () => + this.setState({ + document: { action: '', visible: false, dimmed: false }, + view: { ...this.state.view, disableOnClickOutside: false } + }), + actions: { + create: () => this.documentHandler.show('create'), + update: (id) => this.documentHandler.show('update', { id }) + } + }; + confirmHandler = { + show: (id) => + this.setState({ + confirm: { visible: true, id }, + view: { ...this.state.view, disableOnClickOutside: true } + }), + hide: () => + this.setState({ + confirm: { ...this.state.confirm, visible: false }, + view: { ...this.state.view, disableOnClickOutside: false } + }), + actions: { + delete: () => { + if (this.state.confirm.visible) { + const id = this.state.confirm.id; + this.confirmHandler.hide(); + this.documentHandler.hide(); + this.viewHandler.hide(); + this.props.dispatch(deleteEirRecord(id)); + } + } + } + }; + viewHandler = { + show: (id) => + this.setState({ + view: { id, visible: true, disableOnClickOutside: false } + }), + hide: () => this.setState({ view: { ...this.state.view, visible: false } }) + }; + render() { + const { search, document, view } = this.state; + const { records, status } = this.props; + const data = records.data; + return ( + + {data.length > 0 && ( + + )} + + {records.isLoading && } + + + record._id === view.id)[0]} + disableOnClickOutside={view.disableOnClickOutside} + onEdit={this.documentHandler.actions.update} + onDelete={this.confirmHandler.show} + onHide={this.viewHandler.hide} + /> + + + + + ); + } +} +export default connect((state) => ({ + records: select(fetchEirRecords(), state.crud), + status: selectActionStatus(MODEL, state.crud, 'delete') +}))(Collection); diff --git a/webui/src/containers/Eir/Document.js b/webui/src/containers/Eir/Document.js new file mode 100644 index 000000000..1cc098344 --- /dev/null +++ b/webui/src/containers/Eir/Document.js @@ -0,0 +1,126 @@ +import { Component } from 'react'; +import { connect } from 'react-redux'; +import NProgress from 'nprogress'; +import { + MODEL, + fetchEirRecords, + fetchEirRecord, + createEirRecord, + updateEirRecord +} from 'modules/crud/eir'; +import { clearActionStatus } from 'modules/crud/actions'; +import { select, selectActionStatus } from 'modules/crud/selectors'; +import * as Notification from 'modules/notification/actions'; +import { Eir } from 'components'; +const empty = { pei_type: 'imei', pei: '', imsi: '', status: 'WHITELISTED' }; +const decode = (r) => { + if (!r) return empty; + const p = (r.pei || '').match(/^(imei|imeisv)-(\d+)$/) || []; + const s = (r.supi || '').match(/^imsi-(\d+)$/) || []; + return { + _id: r._id, + pei_type: p[1] || 'imei', + pei: p[2] || '', + imsi: s[1] || '', + status: r.status + }; +}; +const encode = (f) => { + const data = { pei: `${f.pei_type}-${f.pei}`, status: f.status }; + if (f.imsi) data.supi = `imsi-${f.imsi}`; + return data; +}; +class Document extends Component { + state = { formData: empty }; + componentWillMount() { + if (this.props.record.needsFetch) + this.props.dispatch(this.props.record.fetch); + } + componentWillReceiveProps(p) { + if (p.record.needsFetch) p.dispatch(p.record.fetch); + this.setState({ formData: decode(p.record.data) }); + if (p.status.response) { + NProgress.done(true); + p.dispatch( + Notification.success({ + title: 'EIR', + message: + p.action === 'create' + ? 'New EIR rule created' + : `${p.status.id} EIR rule updated` + }) + ); + p.dispatch(clearActionStatus(MODEL, p.action)); + p.onHide(); + } + if (p.status.error) { + NProgress.done(true); + const response = (p.status.error || {}).response || {}; + p.dispatch( + Notification.error({ + title: (response.data || {}).name || response.status || 'EIR Error', + message: + (response.data || {}).message || + response.statusText || + 'Unable to save EIR rule', + autoDismiss: 0, + action: { label: 'Dismiss' } + }) + ); + p.dispatch(clearActionStatus(MODEL, p.action)); + } + } + validate = (f, e) => { + const length = f.pei_type === 'imeisv' ? 16 : 15; + if (f.pei && f.pei.length !== length) + e.pei.addError( + `${f.pei_type.toUpperCase()} must contain ${length} digits` + ); + if (f.imsi && (f.imsi.length < 6 || f.imsi.length > 15)) + e.imsi.addError('IMSI must contain 6 to 15 digits'); + if (this.props.action === 'create') { + const data = encode(f); + const records = this.props.records.data; + if (records.some((r) => + r.pei === data.pei && (r.supi || '') === (data.supi || '') + )) { + e.pei.addError('This equipment identity rule already exists'); + } + } + return e; + }; + submit = (f) => { + NProgress.configure({ parent: '#nprogress-base-form', trickleSpeed: 5 }); + NProgress.start(); + const data = encode(f); + if (this.props.action === 'create') + this.props.dispatch(createEirRecord({}, data)); + else this.props.dispatch(updateEirRecord(f._id, {}, data)); + }; + handleError = (errors) => + errors.map((error) => + this.props.dispatch( + Notification.error({ title: 'Validation Error', message: error.stack }) + ) + ); + render() { + const p = this.props; + return ( + + ); + } +} +export default connect((state, props) => ({ + records: select(fetchEirRecords(), state.crud), + record: select(fetchEirRecord(props.id), state.crud), + status: selectActionStatus(MODEL, state.crud, props.action) +}))(Document); diff --git a/webui/src/containers/Eir/index.js b/webui/src/containers/Eir/index.js new file mode 100644 index 000000000..e0c172253 --- /dev/null +++ b/webui/src/containers/Eir/index.js @@ -0,0 +1,3 @@ +import Collection from './Collection'; +import Document from './Document'; +export { Collection, Document }; diff --git a/webui/src/modules/crud/eir.js b/webui/src/modules/crud/eir.js new file mode 100644 index 000000000..9b35720a3 --- /dev/null +++ b/webui/src/modules/crud/eir.js @@ -0,0 +1,19 @@ +import { + fetchCollection, + fetchDocument, + createDocument, + updateDocument, + deleteDocument +} from './actions'; +export const MODEL = 'eir'; +export const URL = '/Eir'; +export const fetchEirRecords = (params = {}) => + fetchCollection(MODEL, URL, params); +export const fetchEirRecord = (id, params = {}) => + fetchDocument(MODEL, id, `${URL}/${id}`, params); +export const createEirRecord = (params = {}, data = {}) => + createDocument(MODEL, URL, params, data); +export const updateEirRecord = (id, params = {}, data = {}) => + updateDocument(MODEL, id, `${URL}/${id}`, params, data); +export const deleteEirRecord = (id, params = {}) => + deleteDocument(MODEL, id, `${URL}/${id}`, params);