Commit a7f844a3 authored by Muhammad Umair Khan's avatar Muhammad Umair Khan
Browse files

Centralize MEC service mapping, format API console timestamps to YYYY-MM-DD...

Centralize MEC service mapping, format API console timestamps to YYYY-MM-DD HH:mm:ss, and harmonize filter toolbar background styling
parent cde52bf5
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@ meep-session:

meep-oauth-github:
  client-id: "Ov23liZqkabhEidZnJgp"
  secret: "712dfc9374fe192af5833f0735a05b84f54d9a11"
  secret: "a412f4b61cc9b4134f917521db6565153b040761"

meep-oauth-gitlab:
  client-id: "my-gitlab-client-id"
+15 −0
Original line number Diff line number Diff line
@@ -127,3 +127,18 @@ export const ELEMENT_TYPE_MECSVC = 'MEC SERVICE';
export const ELEMENT_TYPE_UE_APP = 'UE APPLICATION';
export const ELEMENT_TYPE_EDGE_APP = 'EDGE APPLICATION';
export const ELEMENT_TYPE_CLOUD_APP = 'CLOUD APPLICATION';

// MEC Services configuration
export const MEC_SERVICES = [
  { prefix: 'meep-app-enablement', code: '011', name: 'App Enablement' },
  { prefix: 'meep-rnis', code: '012', name: 'RNIS' },
  { prefix: 'meep-loc-serv', code: '013', name: 'Location' },
  { prefix: 'meep-tm', code: '015', name: 'Traffic Management' },
  { prefix: 'meep-dai', code: '016', name: 'Device Application Interface' },
  { prefix: 'meep-ams', code: '021', name: 'Application Mobility' },
  { prefix: 'meep-wais', code: '028', name: 'WLAN Access Information' },
  { prefix: 'meep-vis', code: '030', name: 'V2X Information' },
  { prefix: 'meep-iot', code: '033', name: 'IoT API' },
  { prefix: 'meep-federation', code: '040', name: 'Federation API' },
  { prefix: 'meep-sss', code: '046', name: 'SSS API' }
];
+2 −51
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ import { Grid, GridCell } from '@rmwc/grid';


import BasicDialog from './basic-dialog';
import { mapService } from '../../util/string-manipulation';
import {
  DLG_DETAILED_DATA
} from '../../app-constants';
@@ -76,57 +77,7 @@ class DetailedDataDialog extends Component {
    this.classes = props.classes;
  }

  mapService(loggerName) {
    //loggerName is a combination of loggerName and mep name
    let prefixes = ['meep-loc-serv', 'meep-federation', 'meep-rnis', 'meep-dai', 'meep-wais', 'meep-app-enablement', 'meep-ams', 'meep-iot', 'meep-sss', 'meep-vis', 'meep-tm'];
    let prefixStd = '';
    var prefix = '';
    for (var i = 0; i < prefixes.length; i++) {
      if (loggerName.startsWith(prefixes[i])) {
        prefix = prefixes[i];
        break;
      }
    }

    switch(prefix) {
    case 'meep-app-enablement':
      prefixStd = '011';
      break;
    case 'meep-rnis':
      prefixStd = '012';
      break;
    case 'meep-loc-serv':
      prefixStd = '013';
      break;
    case 'meep-tm':
      prefixStd = '015';
      break;
    case 'meep-dai':
      prefixStd = '016';
      break;
    case 'meep-ams':
      prefixStd = '021';
      break;
    case 'meep-wais':
      prefixStd = '028';
      break;
    case 'meep-vis':
      prefixStd = '030';
      break;
    case 'meep-iot':
      prefixStd = '033';
      break;
    case 'meep-federation':
      prefixStd = '040';
      break;
    case 'meep-sss':
      prefixStd = '046';
      break;
    default:
      prefixStd = 'N/A';
    }
    return loggerName.replace(prefix, prefixStd);
  }

  mapDirection(direction) {
    switch(direction) {
@@ -148,7 +99,7 @@ class DetailedDataDialog extends Component {
      return null;
    }

    var serviceId = this.mapService(singleData.loggerName);
    var serviceId = mapService(singleData.loggerName);
    var id = serviceId + '-' + singleData.id;
    var svcName = 'MEC' + serviceId;
    var reqType = this.mapDirection(singleData.direction);
+222 −57
Original line number Diff line number Diff line
@@ -13,13 +13,20 @@ import { connect } from 'react-redux';
import React, { Component } from 'react';
import { withStyles } from '@material-ui/core/styles';
import { getSorting } from '../../util/table-utils';
import { TextField } from '@rmwc/textfield';
import { Select } from '@rmwc/select';
import { Checkbox } from '@rmwc/checkbox';
import { Button } from '@rmwc/button';
import { sboxChangeApiTable } from '../../state/sbox';
import { mapService } from '../../util/string-manipulation';
import {
  uiSandboxChangeApiDetailedData,
  uiChangeCurrentDialog
} from '../../state/ui';
import {
  DIALOG_DETAILED_DATA,
  DEFAULT_NO_NETWORK_FILE_SELECTED
  DEFAULT_NO_NETWORK_FILE_SELECTED,
  MEC_SERVICES
} from '../../app-constants';

const tableStyles = () => ({
@@ -56,9 +63,75 @@ const tableStyles = () => ({
    paddingRight: 0,
    'font-family': 'Rubik',
    whiteSpace: 'nowrap'
  },
  toolbarContainer: {
    display: 'flex',
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    flexWrap: 'wrap',
    padding: '8px 15px',
    borderBottom: '1px solid #d9e2ec',
    backgroundColor: '#ecf3f9',
    gap: '12px'
  },
  filterGroup: {
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    gap: '12px'
  },
  actionGroup: {
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
    gap: '12px'
  },
  filterField: {
    minWidth: '160px',
    width: '160px'
  },
  searchField: {
    minWidth: '180px',
    width: '180px'
  },
  checkboxLabel: {
    display: 'flex',
    alignItems: 'center',
    fontFamily: 'Rubik',
    fontSize: '0.9rem',
    marginLeft: '5px'
  },
  button: {
    textTransform: 'none',
    fontFamily: 'Rubik'
  },
  clearButton: {
    textTransform: 'none',
    fontFamily: 'Rubik',
    color: '#d32f2f',
    borderColor: '#d32f2f'
  }
});

const serviceOptions = [
  { label: 'All Services', value: 'ALL' },
  ...MEC_SERVICES.map(svc => ({
    label: `${svc.code} - ${svc.name}`,
    value: svc.code
  }))
];

const methodOptions = [
  { label: 'All Methods', value: 'ALL' },
  { label: 'GET', value: 'GET' },
  { label: 'POST', value: 'POST' },
  { label: 'PUT', value: 'PUT' },
  { label: 'DELETE', value: 'DELETE' },
  { label: 'PATCH', value: 'PATCH' }
];

const tableColumnData = [
  {
    id: 'id',
@@ -102,6 +175,27 @@ class ApiTable extends Component {
  constructor(props) {
    super(props);
    this.classes = props.classes;
    this.state = {
      searchTerm: '',
      serviceFilter: 'ALL',
      methodFilter: 'ALL',
      errorsOnly: false
    };
  }

  exportLogs(filteredData) {
    if (!filteredData || !filteredData.length) {
      return;
    }
    const jsonString = `data:text/json;charset=utf-8,${encodeURIComponent(
      JSON.stringify(filteredData, null, 2)
    )}`;
    const downloadAnchor = document.createElement('a');
    downloadAnchor.setAttribute('href', jsonString);
    downloadAnchor.setAttribute('download', `api_logs_${new Date().toISOString()}.json`);
    document.body.appendChild(downloadAnchor);
    downloadAnchor.click();
    downloadAnchor.remove();
  }

  onClick(event, row) {
@@ -109,57 +203,7 @@ class ApiTable extends Component {
    this.props.changeCurrentDialog(DIALOG_DETAILED_DATA);
  }

  mapService(loggerName) {
    //loggerName is a combination of loggerName and mep name
    let prefixes = ['meep-loc-serv', 'meep-rnis', 'meep-federation', 'meep-dai', 'meep-wais', 'meep-app-enablement', 'meep-ams', 'meep-vis', 'meep-iot', 'meep-sss', 'meep-tm'];
    let prefixStd = '';
    var prefix = '';
    for (var i = 0; i < prefixes.length; i++) {
      if (loggerName.startsWith(prefixes[i])) {
        prefix = prefixes[i];
        break;
      }
    }

    switch(prefix) {
    case 'meep-loc-serv':
      prefixStd = '013';
      break;
    case 'meep-rnis':
      prefixStd = '012';
      break;
    case 'meep-wais':
      prefixStd = '028';
      break;
    case 'meep-app-enablement':
      prefixStd = '011';
      break;
    case 'meep-ams':
      prefixStd = '021';
      break;
    case 'meep-tm':
      prefixStd = '015';
      break;
    case 'meep-dai':
      prefixStd = '016';
      break;
    case 'meep-vis':
      prefixStd = '030';
      break;
    case 'meep-iot':
      prefixStd = '033';
      break;
    case 'meep-federation':
      prefixStd = '040';
      break;
    case 'meep-sss':
      prefixStd = '046';
      break;
    default:
      prefixStd = 'N/A';
    }
    return loggerName.replace(prefix, prefixStd);
  }

  mapDirection(direction) {
    switch(direction) {
@@ -172,6 +216,27 @@ class ApiTable extends Component {
    }
  }

  formatTime(timeStr) {
    if (!timeStr) {
      return '';
    }
    try {
      const d = new Date(timeStr);
      if (isNaN(d.getTime())) {
        return timeStr;
      }
      const year = d.getFullYear();
      const month = String(d.getMonth() + 1).padStart(2, '0');
      const day = String(d.getDate()).padStart(2, '0');
      const hours = String(d.getHours()).padStart(2, '0');
      const minutes = String(d.getMinutes()).padStart(2, '0');
      const seconds = String(d.getSeconds()).padStart(2, '0');
      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    } catch (e) {
      return timeStr;
    }
  }

  render() {
    const classes = this.classes;
    var data = null;
@@ -182,16 +247,115 @@ class ApiTable extends Component {
    const order = 'desc';
    const orderBy = 'time';

    // Apply filters client-side
    let filteredData = data || [];
    if (this.props.networkFileSelected === DEFAULT_NO_NETWORK_FILE_SELECTED) {
      filteredData = [];
    }

    if (filteredData.length > 0) {
      const { searchTerm, serviceFilter, methodFilter, errorsOnly } = this.state;

      filteredData = filteredData.filter(row => {
        // 1. Search term filter (endpoint)
        if (searchTerm) {
          const endpointLower = (row.endpoint || '').toLowerCase();
          if (!endpointLower.includes(searchTerm.toLowerCase())) {
            return false;
          }
        }

        // 2. Service Filter
        if (serviceFilter !== 'ALL') {
          const serviceMapped = mapService(row.loggerName);
          if (!serviceMapped.startsWith(serviceFilter)) {
            return false;
          }
        }

        // 3. Method Filter
        if (methodFilter !== 'ALL') {
          if ((row.method || '') !== methodFilter) {
            return false;
          }
        }

        // 4. Errors Only
        if (errorsOnly) {
          const code = parseInt(row.respCode, 10);
          if (isNaN(code) || code < 400) {
            return false;
          }
        }

        return true;
      });
    }

    const maxRows = 12;
    var emptyRows = 0;
    if (!data || !data.length) {
    if (!filteredData || !filteredData.length) {
      emptyRows = maxRows;
    } else {
      emptyRows = Math.max(0, maxRows - data.length);
      emptyRows = Math.max(0, maxRows - filteredData.length);
    }

    return (
      <Paper className={classes.root}>
        {/* API Console Toolbar */}
        <div className={classes.toolbarContainer}>
          <div className={classes.filterGroup}>
            <TextField
              outlined
              label="Search endpoint..."
              value={this.state.searchTerm}
              onChange={e => this.setState({ searchTerm: e.target.value })}
              className={classes.searchField}
            />
            <Select
              outlined
              label="Service"
              value={this.state.serviceFilter}
              options={serviceOptions}
              onChange={e => this.setState({ serviceFilter: e.target.value })}
              className={classes.filterField}
            />
            <Select
              outlined
              label="Method"
              value={this.state.methodFilter}
              options={methodOptions}
              onChange={e => this.setState({ methodFilter: e.target.value })}
              className={classes.filterField}
            />
            <Checkbox
              checked={this.state.errorsOnly}
              onChange={e => this.setState({ errorsOnly: e.target.checked })}
              className={classes.checkboxLabel}
            >
              Errors Only
            </Checkbox>
          </div>
          <div className={classes.actionGroup}>
            <Button
              outlined
              className={classes.button}
              onClick={() => this.exportLogs(filteredData)}
              disabled={!filteredData.length}
            >
              Export
            </Button>
            <Button
              outlined
              className={classes.clearButton}
              onClick={() => this.props.clearApiTable()}
              disabled={!data || !data.length}
            >
              Clear
            </Button>
          </div>
        </div>

        <div className={classes.tableWrapper}>
          <Table
            className={classes.table}
@@ -218,7 +382,7 @@ class ApiTable extends Component {
              </TableRow>
            </TableHead>
            {emptyRows < maxRows && <TableBody className={classes.tableBody} title="Click to get detailed info">
              {data
              {filteredData
                .sort(getSorting(order, orderBy))
                .map((row) => {
                  return (
@@ -230,7 +394,7 @@ class ApiTable extends Component {
                      key={row.time}
                    >
                      <TableCell className={classes.tableBodyCellNoWrap} scope="row">
                        {this.mapService(row.loggerName) + '-' + row.id}
                        {mapService(row.loggerName) + '-' + row.id}
                      </TableCell>
                      <TableCell className={classes.tableBodyCell} padding='none'>
                        {row.respCode}
@@ -245,7 +409,7 @@ class ApiTable extends Component {
                        {row.endpoint}
                      </TableCell>
                      <TableCell className={classes.tableBodyCell} padding='none'>
                        {row.time}
                        {this.formatTime(row.time)}
                      </TableCell>
                    </TableRow>
                  );
@@ -287,7 +451,8 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => {
  return {
    changeDetailedData: row => dispatch(uiSandboxChangeApiDetailedData(row)),
    changeCurrentDialog: type => dispatch(uiChangeCurrentDialog(type))
    changeCurrentDialog: type => dispatch(uiChangeCurrentDialog(type)),
    clearApiTable: () => dispatch(sboxChangeApiTable({ values: [] }))
  };
};

+13 −0
Original line number Diff line number Diff line
@@ -2,6 +2,8 @@
 * Copyright (c) 2022 ETSI.  All rights reserved.
 */

import { MEC_SERVICES } from '../app-constants';

export const firstLetterUpper = str => {
  if (!str) {
    return '';
@@ -18,3 +20,14 @@ export const camelCasePrefix = prefix => {
  array[0] = f.charAt(0).toLowerCase() + f.slice(1);
  return array.join('');
};

export const mapService = loggerName => {
  if (!loggerName) {
    return '';
  }
  const svc = MEC_SERVICES.find(s => loggerName.startsWith(s.prefix));
  if (svc) {
    return loggerName.replace(svc.prefix, svc.code);
  }
  return 'N/A' + loggerName;
};