mirror of
https://github.com/open5gs/open5gs.git
synced 2026-10-19 01:42:08 +00:00
[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
This commit is contained in:
parent
e2acd7e3d3
commit
7953b008a8
16 changed files with 820 additions and 2 deletions
|
|
@ -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);
|
||||
|
|
|
|||
19
webui/server/models/eir.js
Normal file
19
webui/server/models/eir.js
Normal 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);
|
||||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
|||
<Icon><SubscriberIcon/></Icon>
|
||||
<Title>Subscriber</Title>
|
||||
</Item>
|
||||
<Item name="eir" selected={selectedView} onSelect={onSelectView}>
|
||||
<Icon><EirIcon/></Icon>
|
||||
<Title>EIR</Title>
|
||||
</Item>
|
||||
<Item name="profile" selected={selectedView} onSelect={onSelectView}>
|
||||
<Icon><ProfileIcon/></Icon>
|
||||
<Title>Profile</Title>
|
||||
|
|
|
|||
74
webui/src/components/Eir/Edit.js
Normal file
74
webui/src/components/Eir/Edit.js
Normal 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);
|
||||
115
webui/src/components/Eir/Item.js
Normal file
115
webui/src/components/Eir/Item.js
Normal 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;
|
||||
47
webui/src/components/Eir/List.js
Normal file
47
webui/src/components/Eir/List.js
Normal 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>
|
||||
);
|
||||
};
|
||||
52
webui/src/components/Eir/Search.js
Normal file
52
webui/src/components/Eir/Search.js
Normal 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>
|
||||
);
|
||||
190
webui/src/components/Eir/View.js
Normal file
190
webui/src/components/Eir/View.js
Normal 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>
|
||||
);
|
||||
};
|
||||
5
webui/src/components/Eir/index.js
Normal file
5
webui/src/components/Eir/index.js
Normal 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 };
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<Layout.Container visible={view === "subscriber"}>
|
||||
<Subscriber.Collection/>
|
||||
</Layout.Container>
|
||||
<Layout.Container visible={view === "eir"}>
|
||||
<Eir.Collection/>
|
||||
</Layout.Container>
|
||||
<Layout.Container visible={view === "profile"}>
|
||||
<Profile.Collection/>
|
||||
</Layout.Container>
|
||||
|
|
|
|||
148
webui/src/containers/Eir/Collection.js
Normal file
148
webui/src/containers/Eir/Collection.js
Normal 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);
|
||||
126
webui/src/containers/Eir/Document.js
Normal file
126
webui/src/containers/Eir/Document.js
Normal 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);
|
||||
3
webui/src/containers/Eir/index.js
Normal file
3
webui/src/containers/Eir/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Collection from './Collection';
|
||||
import Document from './Document';
|
||||
export { Collection, Document };
|
||||
19
webui/src/modules/crud/eir.js
Normal file
19
webui/src/modules/crud/eir.js
Normal 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue