Network Management System

Overview

全球多个 office 网络,netSec,NST…集中管理门户
快速 实时 定位设备告警故障,性能异常,停机原因等
网络监控自动化,设备管理

Demo

Mock 随机数据 在线预览地址 >

技术栈

  • React react-router-dom
  • Node Koa2 Middleware 中间层
  • jest enzyme 单元测试
  • redis key-value 型轻量数据库,用于存储用户界面偏好
  • material-uiUI 库
  • lesscss 预处理
  • eslintairbnb 标准
  • axiospromis 库 前后端对接
  • mockJs拦截 Ajax 请求 生成随机数据
  • umi 框架
  • react-csv/moment/react-dates/react-hook-form第三方库

Mark

History Router

在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面

// server app.js
const send = require("koa-send");
const path = require("path");
app.use(async (ctx, next) => {
  if (ctx.status === 404 && ctx.body === undefined) {
    await send(ctx, path.join(distDirectory, "index.html"));
  }
  await next();
});

这么做以后,node 方 不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。
所以在客户端兜底 404,前端路由配置中加一个 defaultRoute,指向 404component{ path: '/404', component: '@/pages/404/index' },...,{ redirect: '/404' }

User Preference

node 层提供两个 api,一个 get,一个 set

// user-preference.js
const preferenceRouter = new Router();

preferenceRouter.get("/api/preference/", async (ctx, next) => {
  try {
    const {
      user: { Corpid },
    } = ctx.req;
    if (Corpid) {
      const res = await getUserPreference(Corpid);
      ctx.body = { userPreference: res };
      next();
    }
  } catch (error) {
    next();
  }
});
preferenceRouter.post("/api/preference/", async (ctx, next) => {
  try {
    const {
      user: { Corpid },
    } = ctx.req;
    console.log(Corpid);
    const { feature, pageName, value } = ctx.request.body;
    if (Corpid && feature && value) {
      const res = await setUserPreference({
        user: Corpid,
        feature,
        pageName,
        value,
      });
      if (res) ctx.status = 200;
      next();
    }
  } catch (error) {
    next();
  }
});

module.exports = { preferenceRouter };
// app.js
const { preferenceRouter } = require("./user-preference");
app.use(preferenceRouter.routes());

其中 getUserPreference()和 setUserPreference() 操作 redis database,这里使用ioredis as redis client

Redis.Command.setReplyTransformer("hgetall", (result) => {
  if (Array.isArray(result)) {
    const obj = {};
    for (let i = 0; i < result.length; i += 2) {
      obj[result[i]] = result[i + 1];
    }
    return obj;
  }
  return result;
});

const preferenceStore = new Redis({
  ...redisConfig,
  keyPrefix: redisConfig.keyPrefix + "preference:",
});

async function getUserPreference(user) {
  try {
    const res = await preferenceStore.hgetall(user);
    return res;
  } catch (e) {
    throw e;
  }
}

async function setUserPreference({ user, feature, pageName, value }) {
  try {
    const res = await preferenceStore.hset(
      user,
      `${feature}_${pageName}`,
      value
    );
    return true;
  } catch (e) {
    throw e;
  }
}

module.exports = { getUserPreference, setUserPreference };

1: 前端本地应该维护一个全局变量,保存所有最新的 user-preference 值。在用户第一次打开 SPA 时向 node 获取 redis 数据库值;之后前端内部 每次变动 user-preference 时 都要维护这个全局变量(localStorage),hash-url 变动时,使用该全局变量 向 node 最新 redis(生命周期 mounted & unmount & window.onbeforeunload)。
2: 更新 redis 的时间点,可以选在前端每次改动 preference 时同时改变全局变量和向 node api 更新 redis 数据库。也可以在用户销毁 SPA 时向 node 更新 redis(相比前者 不推荐,因为,用户的所有改动都放在一个 api 里传风险大;而且降低每次的传参有助于加快每次 api 请求)

递归 Menu

const SideMenu = ({
  currentMenuLevelArray,
  defaultOpen = false,
  badgetCountObj,
}) => {
  return (
    <List>
      {currentMenuLevelArray.map((currentMenuLevelItem) => (
        <SideMenuItem
          key={currentMenuLevelItem.key}
          currentMenuLevelItem={currentMenuLevelItem}
          defaultOpen={defaultOpen}
          badgetCountObj={badgetCountObj}
        />
      ))}
    </List>
  );
};
const SideMenuItem = ({
  currentMenuLevelItem,
  defaultOpen = false,
  badgetCountObj,
}) => {
  const { location } = useContext(appContext);
  let pathnameArr = location.pathname.split("/");
  document.title = `Lighthouse - ${
    pathnameArr[pathnameArr.length - 1].slice(0, 1).toUpperCase() +
    pathnameArr[pathnameArr.length - 1].slice(1)
  }`;

  const classes = useStyles();
  const [open, setOpen] = React.useState(defaultOpen);
  const LinkenListItem = LinkenListItemFn(
    currentMenuLevelItem.children ? true : false
  );

  return (
    <React.Fragment>
      <LinkenListItem
        button
        currentMenuLevelItem={currentMenuLevelItem}
        open={open}
        clickFn={() => setOpen(!open)}
      >
        {currentMenuLevelItem.icon && (
          <ListItemIcon>
            {React.createElement(currentMenuLevelItem.icon)}
          </ListItemIcon>
        )}
        <Badge
          badgeContent={badgetCountObj[currentMenuLevelItem.key] || 0}
          color="primary"
          max={999}
          // anchorOrigin={{
          //   horizontal: 'left',
          //   vertical: 'top',
          // }}
        >
          <ListItemText
            primary={currentMenuLevelItem.title}
            className={
              currentMenuLevelItem.key === location.pathname
                ? classes.primaryColor
                : null
            }
          />
        </Badge>
      </LinkenListItem>
      {currentMenuLevelItem.children && (
        <Collapse
          in={open}
          timeout="auto"
          unmountOnExit
          className={classes.nested}
        >
          <SideMenu
            currentMenuLevelArray={currentMenuLevelItem.children}
            badgetCountObj={badgetCountObj}
          />
        </Collapse>
      )}
    </React.Fragment>
  );
};

位运算

涉及到 ip 范围(掩码)的 validation,如 10.23.0.0/20 代表前 20 位固定,后 14 位不限,所以前面的 ip 格式需满足在二进制中后 14 位为 0;
AND 位运算 取整,>>> 0去符号(右移 0)

const ipInt = require("ip-to-int");
const [rawIp, bit = 32] = rawNetwork.split("/");
const RawIntIp = ipInt(rawIp).toInt();
const strictIp = ipInt(
  `${(RawIntIp & (2 ** 32 - 2 ** (32 - bit))) >>> 0}`
).toIP();
let result = [[strictIp, bit.toString()].join("/")];

back reminder

netBrain Qapps通过定制 actions,实现智能 track alert cause
zabbix来监控网络 devices 的告警 alerts,应用 SNMP(Simple Network Management Protocol),定时间隔去主动向设备 poll metric or dataset
telemetry vs snmp. 个别设备内支持telemetry协议,设备主动 live update 的发布订阅告警


   转载规则


《Network Management System》 Ryan Who 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录