mirror of
https://github.com/open5gs/open5gs.git
synced 2026-10-19 09:44:04 +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 *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);
|
||||||
|
|
|
||||||
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'
|
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: '',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
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 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
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