diff --git a/fbcnms-packages/fbcnms-ui/components/ActionTable.js b/fbcnms-packages/fbcnms-ui/components/ActionTable.js
index cdb9676..0d09f40 100644
--- a/fbcnms-packages/fbcnms-ui/components/ActionTable.js
+++ b/fbcnms-packages/fbcnms-ui/components/ActionTable.js
@@ -40,11 +40,6 @@ import {makeStyles} from '@material-ui/styles';
import {useState} from 'react';
const useStyles = makeStyles(theme => ({
- inputRoot: {
- '&.MuiOutlinedInput-root': {
- padding: 0,
- },
- },
cardTitleRow: {
marginBottom: theme.spacing(1),
minHeight: '36px',
diff --git a/fbcnms-packages/fbcnms-ui/components/DataGrid.js b/fbcnms-packages/fbcnms-ui/components/DataGrid.js
new file mode 100644
index 0000000..9b7476f
--- /dev/null
+++ b/fbcnms-packages/fbcnms-ui/components/DataGrid.js
@@ -0,0 +1,155 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ * @format
+ */
+import typeof SvgIcon from '@material-ui/core/@@SvgIcon';
+
+import Card from '@material-ui/core/Card';
+import CardHeader from '@material-ui/core/CardHeader';
+import Grid from '@material-ui/core/Grid';
+import React from 'react';
+
+import {brightGray, comet, concrete} from '@fbcnms/ui/theme/colors';
+import {makeStyles} from '@material-ui/styles';
+
+const useStyles = makeStyles(theme => ({
+ dataBlock: {
+ boxShadow: `0 0 0 1px ${concrete}`,
+ },
+ dataLabel: {
+ fontWeight: '500',
+ color: comet,
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ },
+ dataValue: {
+ fontSize: '18px',
+ color: brightGray,
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ width: props =>
+ props.hasStatus
+ ? 'calc(100% - 16px)'
+ : props.hasIcon
+ ? 'calc(100% - 32px)'
+ : '100%',
+ },
+ dataBox: {
+ width: '100%',
+ padding: props => (props.collapsed ? '0' : null),
+
+ '& > div': {
+ width: '100%',
+ },
+ },
+ dataIcon: {
+ display: 'flex',
+ alignItems: 'center',
+
+ '& svg': {
+ fill: comet,
+ marginRight: theme.spacing(1),
+ },
+ },
+ list: {
+ padding: 0,
+ },
+}));
+
+// Data Icon adds an icon to the left of the value
+function DataIcon(icon: SvgIcon, val: string) {
+ const props = {hasIcon: true};
+ const classes = useStyles(props);
+ const Icon = icon;
+ return (
+
+
+
+
+
+ {val}
+
+
+ );
+}
+
+type Data = {
+ icon?: SvgIcon,
+ category?: string,
+ value: number | string,
+ unit?: string,
+ statusCircle?: boolean,
+ statusInactive?: boolean,
+ status?: boolean,
+ tooltip?: string,
+};
+
+export type DataRows = Data[];
+
+type Props = {data: DataRows[], testID?: string};
+
+export default function DataGrid(props: Props) {
+ const classes = useStyles();
+ const dataGrid = props.data.map((row, i) => (
+
+ {row.map((data, j) => {
+ const dataEntryValue = data.value + (data.unit ?? '');
+
+ return (
+
+
+
+
+
+
+
+ );
+ })}
+
+ ));
+ return (
+
+
+ {dataGrid}
+
+
+ );
+}
diff --git a/fbcnms-packages/fbcnms-ui/host/OrganizationDialog.js b/fbcnms-packages/fbcnms-ui/host/OrganizationDialog.js
index abe3a8f..aad251c 100644
--- a/fbcnms-packages/fbcnms-ui/host/OrganizationDialog.js
+++ b/fbcnms-packages/fbcnms-ui/host/OrganizationDialog.js
@@ -7,12 +7,14 @@
* @flow strict-local
* @format
*/
-
+import type {EditUser} from './OrganizationEdit';
import type {OrganizationPlainAttributes} from '@fbcnms/sequelize-models/models/organization';
+import AppContext from '@fbcnms/ui/context/AppContext';
import Button from '@fbcnms/ui/components/design-system/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import LoadingFillerBackdrop from '@fbcnms/ui/components/LoadingFillerBackdrop';
import OrganizationInfoDialog from './OrganizationInfoDialog';
@@ -22,43 +24,77 @@ import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import {UserRoles} from '@fbcnms/auth/types';
-import {brightGray, white} from '@fbcnms/ui/theme/colors';
+import {brightGray, concrete, mirage, white} from '../theme/colors';
import {makeStyles} from '@material-ui/styles';
import {useAxios} from '@fbcnms/ui/hooks';
-import {useState} from 'react';
+import {useContext, useEffect, useState} from 'react';
const useStyles = makeStyles(_ => ({
- input: {
- display: 'inline-flex',
- margin: '5px 0',
- width: '100%',
- },
tabBar: {
backgroundColor: brightGray,
color: white,
},
+ dialog: {
+ backgroundColor: concrete,
+ },
+ dialogActions: {
+ backgroundColor: white,
+ padding: '20px',
+ zIndex: '1',
+ },
+ dialogContent: {
+ padding: '32px',
+ minHeight: '480px',
+ },
+ dialogTitle: {
+ backgroundColor: mirage,
+ padding: '16px 24px',
+ color: white,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ width: '100%',
+ },
}));
+type TabType =
+ | 'automation'
+ | 'admin'
+ | 'inventory'
+ | 'nms'
+ | 'workorders'
+ | 'hub';
export type DialogProps = {
error: string,
- user: CreateUserType,
+ user: EditUser,
organization: OrganizationPlainAttributes,
- onUserChange: CreateUserType => void,
+ onUserChange: EditUser => void,
onOrganizationChange: OrganizationPlainAttributes => void,
// Array of networks ids
allNetworks: Array,
// If true, enable all networks for an organization
shouldEnableAllNetworks: boolean,
setShouldEnableAllNetworks: boolean => void,
+ edit: boolean,
+ getProjectTabs?: () => Array<{id: TabType, name: string}>,
+ // flag to display advanced fields
+ hideAdvancedFields: boolean,
};
type Props = {
onClose: () => void,
- onCreateOrg: (org: CreateOrgType) => void,
+ onCreateOrg: (org: $Shape) => void,
onCreateUser: (user: CreateUserType) => void,
// flag to display create user tab
addUser: boolean,
setAddUser: () => void,
+ user: ?EditUser,
+ open: boolean,
+ organization: ?OrganizationPlainAttributes,
+ // editing organization
+ edit: boolean,
+ // flag to display advanced fields
+ hideAdvancedFields: boolean,
};
type CreateUserType = {
@@ -68,15 +104,10 @@ type CreateUserType = {
organization?: string,
role: ?string,
tabs?: Array,
- password: string,
+ password: ?string,
passwordConfirmation?: string,
};
-type CreateOrgType = {
- name: string,
- networkIDs: Array,
- customDomains?: Array,
-};
/**
* Create Orgnization Dilaog
* This component displays a dialog with 2 tabs
@@ -84,6 +115,7 @@ type CreateOrgType = {
* Second tab: OrganizationUserDialog, to create a user that belongs to the new organization
*/
export default function (props: Props) {
+ const {ssoEnabled} = useContext(AppContext);
const classes = useStyles();
const {error, isLoading, response} = useAxios({
method: 'get',
@@ -91,23 +123,38 @@ export default function (props: Props) {
});
const [organization, setOrganization] = useState(
- {},
+ props.organization || {},
);
- const [currentTab, setCurrentTab] = useState(props.addUser ? 1 : 0);
+ const [currentTab, setCurrentTab] = useState(0);
const [shouldEnableAllNetworks, setShouldEnableAllNetworks] = useState(false);
- const [user, setUser] = useState({});
+ const [user, setUser] = useState(props.user || {});
const [createError, setCreateError] = useState('');
const allNetworks = error || !response ? [] : response.data.sort();
+ const organizationDialogTitle =
+ currentTab === 1
+ ? 'Add User'
+ : props.edit
+ ? 'Edit Organization'
+ : 'Add Organization';
+
+ useEffect(() => {
+ setCurrentTab(props.addUser ? 1 : 0);
+ }, [props.addUser]);
+
+ useEffect(() => {
+ setOrganization(props.organization || {});
+ setCreateError('');
+ setUser(props.user || {});
+ }, [props.open, props.organization, props.user]);
if (isLoading) {
return ;
}
-
const createProps = {
user,
organization,
error: createError,
- onUserChange: (user: CreateUserType) => {
+ onUserChange: (user: EditUser) => {
setUser(user);
},
onOrganizationChange: (organization: OrganizationPlainAttributes) => {
@@ -116,6 +163,8 @@ export default function (props: Props) {
allNetworks,
shouldEnableAllNetworks,
setShouldEnableAllNetworks,
+ edit: props.edit,
+ hideAdvancedFields: props.hideAdvancedFields,
};
const onSave = async () => {
if (currentTab === 0) {
@@ -123,35 +172,44 @@ export default function (props: Props) {
setCreateError('Name cannot be empty');
return;
}
- const payload = {
+ const newOrg = {
name: organization.name,
networkIDs: shouldEnableAllNetworks
? allNetworks
: Array.from(organization.networkIDs || []),
customDomains: [], // TODO
- // tabs: Array.from(tabs),
+ // default tab is nms
+ tabs: Array.from(organization.tabs || ['nms']),
+ csvCharset: organization.csvCharset,
+ ssoSelectedType: organization.ssoSelectedType,
+ ssoCert: organization.ssoCert,
+ ssoEntrypoint: organization.ssoEntrypoint,
+ ssoIssuer: organization.ssoIssuer,
+ ssoOidcClientID: organization.ssoOidcClientID,
+ ssoOidcClientSecret: organization.ssoOidcClientSecret,
+ ssoOidcConfigurationURL: organization.ssoOidcConfigurationURL,
};
- props.onCreateOrg(payload);
+ props.onCreateOrg(newOrg);
setCurrentTab(currentTab + 1);
setCreateError('');
props.setAddUser();
} else {
- if (!user.email) {
+ if (user.password != user.passwordConfirmation) {
+ setCreateError('Passwords must match');
+ return;
+ }
+ if (!user?.email) {
setCreateError('Email cannot be empty');
return;
}
- if (!user.password) {
+ if ((!user?.password ?? false) && !ssoEnabled && !user.id) {
setCreateError('Password cannot be empty');
return;
}
- if (user.password != user.passwordConfirmation) {
- setCreateError('Passwords must match');
- return;
- }
- const payload: CreateUserType = {
+ const newUser: CreateUserType = {
email: user.email,
password: user.password,
role: user.role,
@@ -160,35 +218,46 @@ export default function (props: Props) {
? []
: Array.from(user.networkIDs || []),
};
- props.onCreateUser(payload);
+ if ((user.id || ssoEnabled) && !user?.password) {
+ delete newUser.password;
+ }
+ props.onCreateUser(newUser);
}
};
return (
diff --git a/fbcnms-packages/fbcnms-ui/host/OrganizationEdit.js b/fbcnms-packages/fbcnms-ui/host/OrganizationEdit.js
index e2d0695..40d0ed1 100644
--- a/fbcnms-packages/fbcnms-ui/host/OrganizationEdit.js
+++ b/fbcnms-packages/fbcnms-ui/host/OrganizationEdit.js
@@ -9,111 +9,108 @@
*/
import type {Organization} from './Organizations';
-import type {SSOSelectedType} from '@fbcnms/types/auth';
import type {Tab} from '@fbcnms/types/tabs';
+import type {WithAlert} from '@fbcnms/ui/components/Alert/withAlert';
+import ArrowBackIcon from '@material-ui/icons/ArrowBack';
import Button from '@fbcnms/ui/components/design-system/Button';
-import Checkbox from '@material-ui/core/Checkbox';
-import FormControl from '@material-ui/core/FormControl';
-import FormControlLabel from '@material-ui/core/FormControlLabel';
-import FormGroup from '@material-ui/core/FormGroup';
import Grid from '@material-ui/core/Grid';
-import Input from '@material-ui/core/Input';
-import InputLabel from '@material-ui/core/InputLabel';
-import ListItemText from '@material-ui/core/ListItemText';
+import IconButton from '@material-ui/core/IconButton';
import LoadingFiller from '@fbcnms/ui/components/LoadingFiller';
-import MenuItem from '@material-ui/core/MenuItem';
-import Paper from '@material-ui/core/Paper';
+import OrganizationDialog from './OrganizationDialog';
+import OrganizationSummary from './OrganizationSummary';
+import OrganizationUsersTable from './OrganizationUsersTable';
import React from 'react';
-import SaveIcon from '@material-ui/icons/Save';
-import Select from '@material-ui/core/Select';
import Text from '@fbcnms/ui/components/design-system/Text';
-import TextField from '@material-ui/core/TextField';
-import TypedSelect from '@fbcnms/ui/components/TypedSelect';
import axios from 'axios';
-import renderList from '@fbcnms/util/renderList';
-import symphony from '@fbcnms/ui/theme/symphony';
-import {getProjectTabs as getAllProjectTabs} from '@fbcnms/projects/projects';
-import {intersection} from 'lodash';
+import withAlert from '@fbcnms/ui/components/Alert/withAlert';
+
import {makeStyles} from '@material-ui/styles';
import {useAxios, useRouter} from '@fbcnms/ui/hooks';
-import {useCallback, useEffect, useState} from 'react';
+import {useCallback, useState} from 'react';
import {useEnqueueSnackbar} from '@fbcnms/ui/hooks/useSnackbar';
-const useStyles = makeStyles(theme => ({
+const useStyles = makeStyles(_ => ({
+ arrowBack: {
+ paddingRight: '0px',
+ color: 'black',
+ },
+ container: {
+ margin: '40px 32px',
+ },
header: {
margin: '10px',
display: 'flex',
justifyContent: 'space-between',
+ textTransform: 'capitalize',
},
- leftIcon: {
- marginRight: theme.spacing(),
- color: symphony.palette.white,
- },
- root: {
- ...theme.mixins.gutters(),
- paddingTop: theme.spacing(2),
- paddingBottom: theme.spacing(2),
- },
- textField: {
- marginLeft: theme.spacing(),
- marginRight: theme.spacing(),
- width: 500,
- },
- networks: {
- flexDirection: 'row',
- flexWrap: 'nowrap',
- marginTop: '16px',
- marginBottom: '8px',
- },
- flexGrow: {
- flexGrow: 1,
+ titleRow: {
+ margin: '16px 0',
},
}));
+export type EditUser = {
+ id: string | number,
+ email: string,
+ role: string,
+ networkIDs?: string[],
+ organization?: string,
+ tabs?: Array,
+ password?: string,
+ passwordConfirmation?: string,
+};
+type TitleRowProps = {
+ title: string,
+ buttonTitle: string,
+ onClick: () => void,
+};
type Props = {
getProjectTabs?: () => Array<{id: Tab, name: string}>,
+ hideAdvancedFields?: boolean,
};
-export default function OrganizationEdit(props: Props) {
- const {match} = useRouter();
+function TitleRow(props: TitleRowProps) {
+ const classes = useStyles();
+ return (
+
+ {props.title}
+ props.onClick()}>
+
+ {props.buttonTitle}
+
+
+
+ );
+}
+
+/**
+ * Organization detail view and Organization edit dialog
+ * This component displays an Organization basic information (OrganizationSummary)
+ * and its users (OrganizationUsersTable)
+ */
+function OrganizationEdit(props: WithAlert & Props) {
+ const {match, history} = useRouter();
+ const [addUser, setAddUser] = useState(false);
const classes = useStyles();
const enqueueSnackbar = useEnqueueSnackbar();
const [name, setName] = useState('');
- const [csvCharset, setCsvCharset] = useState('');
- const [tabs, setTabs] = useState>(new Set());
- const [shouldEnableAllNetworks, setShouldEnableAllNetworks] = useState(false);
const [networkIds, setNetworkIds] = useState>(new Set());
- const [ssoSelectedType, setSSOSelectedType] = useState(
- 'saml',
- );
- const [ssoIssuer, setSSOIssuer] = useState('');
- const [ssoEntrypoint, setSSOEntrypoint] = useState('');
- const [ssoCert, setSSOCert] = useState('');
- const [ssoOidcClientID, setSSOOidcClientID] = useState('');
- const [ssoOidcClientSecret, setSSOOidcClientSecret] = useState('');
- const [
- ssoOidcConfigurationURL,
- setSSOOidcConfigurationURL,
- ] = useState('');
-
+ const [dialog, setDialog] = useState(false);
+ const [createError, setCreateError] = useState('');
+ const [refresh, setRefresh] = useState(Date.now());
+ const [user, setUser] = useState(null);
+ const [editUser, setEditUser] = useState(false);
const orgRequest = useAxios({
method: 'get',
url: '/host/organization/async/' + match.params.name,
- onResponse: useCallback(res => {
- const {organization} = res.data;
- setName(organization.name);
- setTabs(new Set(organization.tabs));
- setNetworkIds(new Set(organization.networkIDs));
- setCsvCharset(organization.csvCharset);
- setSSOSelectedType(organization.ssoSelectedType);
- setSSOIssuer(organization.ssoIssuer);
- setSSOEntrypoint(organization.ssoEntrypoint);
- setSSOCert(organization.ssoCert);
- setSSOOidcClientID(organization.ssoOidcClientID);
- setSSOOidcClientSecret(organization.ssoOidcClientSecret);
- setSSOOidcConfigurationURL(organization.ssoOidcConfigurationURL);
- }, []),
+ onResponse: useCallback(
+ res => {
+ const {organization} = res.data;
+ setName(organization.name);
+ setNetworkIds(new Set(organization.networkIDs));
+ },
+ [refresh],
+ ),
});
const networksRequest = useAxios({
@@ -121,16 +118,6 @@ export default function OrganizationEdit(props: Props) {
url: '/host/networks/async',
});
- useEffect(() => {
- if (orgRequest.response && networksRequest.response) {
- const allNetworks: string[] = networksRequest.response.data;
- const networkIDs = orgRequest.response.data.organization.networkIDs;
- if (intersection(allNetworks, networkIDs).length === allNetworks.length) {
- setShouldEnableAllNetworks(true);
- }
- }
- }, [orgRequest.response, networksRequest.response]);
-
if (
orgRequest.isLoading ||
networksRequest.isLoading ||
@@ -140,28 +127,10 @@ export default function OrganizationEdit(props: Props) {
}
const organization = orgRequest.response.data.organization;
- const allNetworks =
- networksRequest.error || !networksRequest.response
- ? []
- : networksRequest.response.data.sort();
- const onSave = () => {
+ const onSave = org => {
axios
- .put('/host/organization/async/' + match.params.name, {
- name,
- tabs: Array.from(tabs),
- networkIDs: shouldEnableAllNetworks
- ? allNetworks
- : Array.from(networkIds),
- csvCharset,
- ssoSelectedType,
- ssoIssuer,
- ssoEntrypoint,
- ssoCert,
- ssoOidcClientID,
- ssoOidcClientSecret,
- ssoOidcConfigurationURL,
- })
+ .put('/host/organization/async/' + match.params.name, org)
.then(_res => {
enqueueSnackbar('Updated organization successfully', {
variant: 'success',
@@ -175,205 +144,149 @@ export default function OrganizationEdit(props: Props) {
});
};
- const allTabs = props.getProjectTabs
- ? props.getProjectTabs()
- : getAllProjectTabs();
-
return (
-
-
-
- Organization: {organization.name}
-
-
-
-
-
- Basic Info
-