[EIR] add WebUI equipment identity management (#4795)
Some checks are pending
CIFuzz / Fuzzing (push) Waiting to run
Meson Continuous Integration / Build and Test on Ubuntu Latest (push) Waiting to run

* 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
This commit is contained in:
ygzaydn 2026-09-23 05:21:37 +03:00 • committed by GitHub
parent e2acd7e3d3
commit 7953b008a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 820 additions and 2 deletions

View file

@ -36,6 +36,7 @@ static ogs_thread_t *pcf_thread = NULL;
static ogs_thread_t *nssf_thread = NULL; static ogs_thread_t *nssf_thread = NULL;
static ogs_thread_t *bsf_thread = NULL; static ogs_thread_t *bsf_thread = NULL;
static ogs_thread_t *udr_thread = NULL; static ogs_thread_t *udr_thread = NULL;
static ogs_thread_t *eir_thread = NULL;
int app_initialize(const char *const argv[]) 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) if (ogs_global_conf()->parameter.no_udr == 0)
udr_thread = test_child_create("udr", 0, argv_out); 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 * Wait for all sockets listening
* *
@ -118,6 +123,7 @@ void app_terminate(void)
if (upf_thread) ogs_thread_destroy(upf_thread); if (upf_thread) ogs_thread_destroy(upf_thread);
if (udr_thread) ogs_thread_destroy(udr_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 (bsf_thread) ogs_thread_destroy(bsf_thread);
if (nssf_thread) ogs_thread_destroy(nssf_thread); if (nssf_thread) ogs_thread_destroy(nssf_thread);
if (pcf_thread) ogs_thread_destroy(pcf_thread); if (pcf_thread) ogs_thread_destroy(pcf_thread);

View file

@ -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);

View file

@ -10,6 +10,9 @@ restify.serve(router, Subscriber, {
idProperty: 'imsi' idProperty: 'imsi'
}); });
const Eir = require('../models/eir');
restify.serve(router, Eir, { prefix: '', version: '' });
const Profile = require('../models/profile'); const Profile = require('../models/profile');
restify.serve(router, Profile, { restify.serve(router, Profile, {
prefix: '', prefix: '',

View file

@ -7,6 +7,7 @@ import oc from 'open-color';
import SubscriberIcon from 'react-icons/lib/md/person-add'; import SubscriberIcon from 'react-icons/lib/md/person-add';
import ProfileIcon from 'react-icons/lib/md/content-copy'; import ProfileIcon from 'react-icons/lib/md/content-copy';
import AccountIcon from 'react-icons/lib/md/vpn-key'; import AccountIcon from 'react-icons/lib/md/vpn-key';
import EirIcon from 'react-icons/lib/md/phonelink-lock';
const Menu = styled.div` const Menu = styled.div`
display: block; display: block;
@ -85,6 +86,10 @@ const Sidebar = ({ isOpen, width, selectedView, onSelectView }) => (
<Icon><SubscriberIcon/></Icon> <Icon><SubscriberIcon/></Icon>
<Title>Subscriber</Title> <Title>Subscriber</Title>
</Item> </Item>
<Item name="eir" selected={selectedView} onSelect={onSelectView}>
<Icon><EirIcon/></Icon>
<Title>EIR</Title>
</Item>
<Item name="profile" selected={selectedView} onSelect={onSelectView}> <Item name="profile" selected={selectedView} onSelect={onSelectView}>
<Icon><ProfileIcon/></Icon> <Icon><ProfileIcon/></Icon>
<Title>Profile</Title> <Title>Profile</Title>

View file

@ -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 (
<Form
visible={p.isLoading ? false : p.visible}
title={p.action === 'update' ? 'Edit EIR Rule' : 'Create EIR Rule'}
width="480px"
height="440px"
schema={schema}
uiSchema={this.state.uiSchema}
formData={this.state.formData}
isLoading={p.isLoading}
validate={p.validate}
onHide={p.onHide}
onSubmit={p.onSubmit}
onError={p.onError}
/>
);
}
}
export default withWidth()(Edit);

View file

@ -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 (
<Sizer disabled={disabled}>
<Card disabled={disabled} onClick={() => onView(record._id)}>
<Pei>{displayPei(record.pei)}</Pei>
<div className="actions">
<Tooltip content="Edit" width="60px">
<CircleButton onClick={this.handleEdit}>
<EditIcon />
</CircleButton>
</Tooltip>
<Tooltip content="Delete" width="60px">
<CircleButton className="delete" onClick={this.handleDelete}>
<DeleteIcon />
</CircleButton>
</Tooltip>
</div>
{disabled && (
<SpinnerWrapper>
<Spinner sm />
</SpinnerWrapper>
)}
</Card>
</Sizer>
);
}
}
export default Item;

View file

@ -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) => (
<Item
key={record._id}
record={record}
disabled={deletedId === record._id}
onView={onView}
onEdit={onEdit}
onDelete={onDelete}
/>
));
return (
<Wrapper>
<CSSTransitionGroup
transitionName="eir"
transitionEnterTimeout={300}
transitionLeaveTimeout={150}
>
{list}
</CSSTransitionGroup>
</Wrapper>
);
};

View file

@ -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 }) => (
<Wrapper>
<Icon>
<SearchIcon />
</Icon>
<Input value={value} onChange={onChange} />
{value !== '' && (
<Clear onClick={onClear}>
<ClearIcon />
</Clear>
)}
</Wrapper>
);

View file

@ -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 (
<Modal
visible={visible}
onOutside={onHide}
disableOnClickOutside={disableOnClickOutside}
>
<Wrapper>
<Header>
<div className="title">{raw(r.pei)}</div>
<div className="actions">
<Tooltip content="Edit" width="60px">
<CircleButton onClick={() => onEdit(r._id)}>
<EditIcon />
</CircleButton>
</Tooltip>
<Tooltip content="Delete" width="60px">
<CircleButton className="delete" onClick={() => onDelete(r._id)}>
<DeleteIcon />
</CircleButton>
</Tooltip>
<Tooltip content="Close" width="60px">
<CircleButton className="delete" onClick={onHide}>
<CloseIcon />
</CircleButton>
</Tooltip>
</div>
</Header>
<Body>
<Details>
<div className="header">Equipment Identity Rule</div>
<div className="body">
<div className="left">
<PhoneIcon />
</div>
<div className="right">
<div className="data">
{(r.pei || '').split('-')[0].toUpperCase()}
<span style={{ color: oc.gray[5] }}>
<KeyboardControlIcon />
Identity Type
</span>
</div>
<div className="data">
{raw(r.pei)}
<span style={{ color: oc.gray[5] }}>
<KeyboardControlIcon />
IMEI / IMEISV
</span>
</div>
{r.supi && (
<div className="data">
{raw(r.supi)}
<span style={{ color: oc.gray[5] }}>
<KeyboardControlIcon />
IMSI
</span>
</div>
)}
</div>
</div>
<div className="body">
<div className="left">
<SecurityIcon />
</div>
<div className="right">
<div className="data">
{r.supi ? 'Subscriber Rule' : 'Device Rule'}
<span style={{ color: oc.gray[5] }}>
<KeyboardControlIcon />
Rule Type
</span>
</div>
<div className="data">
{r.status}
<span style={{ color: oc.gray[5] }}>
<KeyboardControlIcon />
Equipment Status
</span>
</div>
</div>
</div>
</Details>
</Body>
</Wrapper>
</Modal>
);
};

View file

@ -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 };

View file

@ -19,6 +19,7 @@ import Confirm from './Shared/Confirm';
import * as Subscriber from './Subscriber'; import * as Subscriber from './Subscriber';
import * as Profile from './Profile'; import * as Profile from './Profile';
import * as Account from './Account'; import * as Account from './Account';
import * as Eir from './Eir';
export { export {
Layout, Layout,
@ -41,5 +42,6 @@ export {
Subscriber, Subscriber,
Profile, Profile,
Account Account,
Eir
} }

View file

@ -11,6 +11,7 @@ import Notification from 'containers/Notification';
import * as Subscriber from 'containers/Subscriber'; import * as Subscriber from 'containers/Subscriber';
import * as Profile from 'containers/Profile'; import * as Profile from 'containers/Profile';
import * as Account from 'containers/Account'; import * as Account from 'containers/Account';
import * as Eir from 'containers/Eir';
class App extends Component { class App extends Component {
static propTypes = { static propTypes = {
@ -36,7 +37,7 @@ class App extends Component {
session session
} = this.props; } = this.props;
if (view === "subscriber") { if (view === "subscriber" || view === "eir") {
document.body.style.backgroundColor = "#e9ecef"; document.body.style.backgroundColor = "#e9ecef";
} else { } else {
document.body.style.backgroundColor = "white"; document.body.style.backgroundColor = "white";
@ -47,6 +48,9 @@ class App extends Component {
<Layout.Container visible={view === "subscriber"}> <Layout.Container visible={view === "subscriber"}>
<Subscriber.Collection/> <Subscriber.Collection/>
</Layout.Container> </Layout.Container>
<Layout.Container visible={view === "eir"}>
<Eir.Collection/>
</Layout.Container>
<Layout.Container visible={view === "profile"}> <Layout.Container visible={view === "profile"}>
<Profile.Collection/> <Profile.Collection/>
</Layout.Container> </Layout.Container>

View file

@ -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 (
<Layout.Content>
{data.length > 0 && (
<Eir.Search
value={search}
onChange={this.handleSearchChange}
onClear={this.handleSearchClear}
/>
)}
<Eir.List
records={data}
deletedId={status.id}
onView={this.viewHandler.show}
onEdit={this.documentHandler.actions.update}
onDelete={this.confirmHandler.show}
search={search}
/>
{records.isLoading && <Spinner md />}
<Blank
visible={!records.isLoading && data.length === 0}
title="ADD AN EIR RULE"
body="You have no equipment identity rules... yet!"
onTitle={this.documentHandler.actions.create}
/>
<FloatingButton onClick={this.documentHandler.actions.create} />
<Eir.View
visible={view.visible}
record={data.filter((record) => record._id === view.id)[0]}
disableOnClickOutside={view.disableOnClickOutside}
onEdit={this.documentHandler.actions.update}
onDelete={this.confirmHandler.show}
onHide={this.viewHandler.hide}
/>
<Document {...document} onHide={this.documentHandler.hide} />
<Dimmed visible={document.dimmed || view.visible} />
<Confirm
visible={this.state.confirm.visible}
message="Delete this EIR rule?"
onOutside={this.confirmHandler.hide}
buttons={[
{ text: 'CANCEL', action: this.confirmHandler.hide, info: true },
{
text: 'DELETE',
action: this.confirmHandler.actions.delete,
danger: true
}
]}
/>
</Layout.Content>
);
}
}
export default connect((state) => ({
records: select(fetchEirRecords(), state.crud),
status: selectActionStatus(MODEL, state.crud, 'delete')
}))(Collection);

View file

@ -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 (
<Eir.Edit
visible={p.visible}
action={p.action}
formData={this.state.formData}
isLoading={p.record.isLoading && !p.status.pending}
validate={this.validate}
onHide={p.onHide}
onSubmit={this.submit}
onError={this.handleError}
/>
);
}
}
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);

View file

@ -0,0 +1,3 @@
import Collection from './Collection';
import Document from './Document';
export { Collection, Document };

View file

@ -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);