Skip to content

Commit

Permalink
feat(tgnms): sites file editor
Browse files Browse the repository at this point in the history
Summary:
Adds an editor for the sites file. Previously users had to download the sites-file, edit it in an external program, and reupload it. Now it can be edited directly from the NMS.

{F695985117}

## Sitesfile Editor
* drag to move sites on the map
* create and bulk copy/delete sites

# Other changes

**Manage input file**
Replaces the old browse button with an autocomplete.

{F695985593}
{F695985594}

**URLs**

Previously the PlansTable would navigate to /plan if the plan was successful, rendering the TopologyTable. Now it navigates to /plan and renders the PlanView. PlanView renders the TopologyTable if the plan is successful, SitesFileEditor if the plan is in the draft state, and redirects away from /plan otherwise.

Success
{F695986349}
Error
{F695986355}
Draft
{F695986354}

Reviewed By: tommyhuynh

Differential Revision: D31818889

fbshipit-source-id: 0f8d406e729144d79da8eb41c3abee759c83457f
  • Loading branch information
aclave1 authored and facebook-github-bot committed Feb 1, 2022
1 parent 4bdc2c7 commit ac57db8
Show file tree
Hide file tree
Showing 57 changed files with 2,770 additions and 1,000 deletions.
53 changes: 50 additions & 3 deletions tgnms/fbcnms-projects/tgnms/app/apiutils/NetworkPlanningAPIUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
ANPFileHandle,
ANPFileHandleRequest,
ANPPlanMetrics,
FileRoles,
GraphQueryResponse,
} from '@fbcnms/tg-nms/shared/dto/ANP';
import type {
Expand All @@ -20,6 +21,7 @@ import type {
NetworkPlan,
PlanError,
PlanFolder,
SitesFile,
UpdateNetworkPlanRequest,
} from '@fbcnms/tg-nms/shared/dto/NetworkPlan';
import type {
Expand Down Expand Up @@ -84,7 +86,7 @@ export async function uploadANPFile({
uploadChunkSize,
}: {
name: string,
role: string,
role: FileRoles,
file: File,
// used for testing
uploadChunkSize?: number,
Expand Down Expand Up @@ -212,9 +214,25 @@ export async function cancelPlan(req: {id: number}) {
return response.data;
}

export async function getPartnerFiles({role}: {role: string}) {
/**
* Gets input files from the NMS DB. These don't have to be
* associated with any plan.
*/
export async function getInputFiles({
role,
}: {
role: FileRoles,
}): Promise<Array<InputFile>> {
const response = await axios.get<void, Array<InputFile>>(
`/network_plan/inputs?role=${role}`,
);
return response.data;
}

// DEPRECATED
export async function getPartnerFiles({role}: {role: FileRoles}) {
const response = await axios<
{role: string},
{role: FileRoles},
GraphQueryResponse<ANPFileHandle>,
>({
url: `/network_plan/file?role=${role}`,
Expand Down Expand Up @@ -317,6 +335,7 @@ export async function getPlanInputFiles({
});
return response.data;
}

export async function getPlanOutputFiles({
id,
}: {
Expand Down Expand Up @@ -357,3 +376,31 @@ export async function downloadANPFile<T>({id}: {id: string}): Promise<T> {
});
return response.data;
}

type CreateSitesFileRequest = {|
name: string,
|};
export async function createSitesFile(data: CreateSitesFileRequest) {
const response = await axios<CreateSitesFileRequest, InputFile>({
url: `/network_plan/sites`,
method: 'POST',
data: data,
});
return response.data;
}
export async function updateSitesFile(data: SitesFile) {
const response = await axios<SitesFile, SitesFile>({
url: `/network_plan/sites/${data.id}`,
method: 'PUT',
data: data,
});
return response.data;
}

export async function getSitesFile({id}: {id: number}): Promise<SitesFile> {
const response = await axios<void, SitesFile>({
url: `/network_plan/sites/${id}`,
method: 'GET',
});
return response.data;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import MaterialTable from '@material-table/core';
import Remove from '@material-ui/icons/Remove';
import SaveAlt from '@material-ui/icons/SaveAlt';
import Search from '@material-ui/icons/Search';
import TableToolbar, {TableToolbarAction} from './MaterialTableToolbar';
import ViewColumn from '@material-ui/icons/ViewColumn';
import {defaultProps as MaterialTableDefaultProps} from '@material-table/core/dist/defaults';

Expand Down Expand Up @@ -146,12 +147,18 @@ const defaultTableOptions = {
},
emptyRowsWhenPaging: false,
actionsColumnIndex: -1,
grouping: false,
};
function EditField(props) {
const Component = MaterialTableDefaultProps.components.EditField;
return <Component {...props} variant="outlined" />;
}

export default function CustomMaterialTable({
options,
tableRef: tableRefProp,
components,
'data-testid': testId,
...props
}: Object) {
const {
Expand All @@ -176,11 +183,14 @@ export default function CustomMaterialTable({
);
const _components = React.useMemo(
() => ({
EditField,
/**
* to measure the table height, pass a ref to the Container component and
* measure the mounted element.
*/
Container: Container,
Toolbar: TableToolbar,
Action: TableToolbarAction,
...(components ?? {}),
}),
[Container, components],
Expand All @@ -196,7 +206,10 @@ export default function CustomMaterialTable({
);

return (
<div style={{height: '100%'}} ref={wrapperRef}>
<div
style={{height: '100%'}}
ref={wrapperRef}
data-testid={testId ?? 'material-table'}>
<MaterialTable
icons={tableIcons}
{...props}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
*/
import * as React from 'react';
import MaterialTable, {useAutosizeMaterialTable} from '../MaterialTable';
import {TestApp} from '@fbcnms/tg-nms/app/tests/testHelpers';
import {act as hooksAct, renderHook} from '@testing-library/react-hooks';
import {render} from '@testing-library/react';

test('renders', () => {
const {getByText} = render(
<MaterialTable
columns={[{field: 'name', title: 'name'}]}
data={[{name: 'test1'}, {name: 'test2'}]}
/>,
<TestApp>
<MaterialTable
columns={[{field: 'name', title: 'name'}]}
data={[{name: 'test1'}, {name: 'test2'}]}
/>
</TestApp>,
);

expect(getByText('name')).toBeInTheDocument();
Expand Down
1 change: 1 addition & 0 deletions tgnms/fbcnms-projects/tgnms/app/constants/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const NETWORK_BASE = '/:view/:networkName';
export const PLANNING_BASE_PATH = NETWORK_BASE + '/planning';
export const PLANNING_FOLDER_PATH = PLANNING_BASE_PATH + '/folder/:folderId';
export const PLANNING_PLAN_PATH = PLANNING_FOLDER_PATH + '/plan';
export const PLANNING_SITESFILE_PATH = PLANNING_FOLDER_PATH + '/sites';
export const NETWORK_TABLES_BASE_PATH = NETWORK_BASE + '/:table?';
export const MAP_PATH = `/map/:networkName`;
export const TABLES_PATH = `/tables/:networkName`;
Expand Down
130 changes: 91 additions & 39 deletions tgnms/fbcnms-projects/tgnms/app/contexts/NetworkPlanningContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import {useStateWithTaskState} from '@fbcnms/tg-nms/app/helpers/ContextHelpers';
import type {ANPFileHandle} from '@fbcnms/tg-nms/shared/dto/ANP';
import type {ANPUploadTopologyType} from '@fbcnms/tg-nms/app/constants/TemplateConstants';
import type {MapOptionsState} from '@fbcnms/tg-nms/app/features/planning/PlanningHelpers';
import type {NetworkPlan} from '@fbcnms/tg-nms/shared/dto/NetworkPlan';
import type {PlanFolder} from '@fbcnms/tg-nms/shared/dto/NetworkPlan';
import type {
NetworkPlan,
PlanFolder,
SitesFile,
} from '@fbcnms/tg-nms/shared/dto/NetworkPlan';
import type {SetState} from '@fbcnms/tg-nms/app/helpers/ContextHelpers';
import type {TaskState} from '@fbcnms/tg-nms/app/hooks/useTaskState';

Expand Down Expand Up @@ -88,8 +91,17 @@ export type NetworkPlanningContext = {|
outputFiles: ?Array<ANPFileHandle>,
setOutputFiles: SetState<?Array<ANPFileHandle>>,
loadOutputFilesTask: TaskState,

downloadOutputTask: TaskState,

// sites-file editor
sitesFile: ?SitesFile,
setSitesFile: SetState<?SitesFile>,
sitesFileTask: TaskState,
selectedSites: Array<number>,
setSelectedSites: SetState<Array<number>>,
pendingSitesFile: ?SitesFile,
setPendingSitesFile: SetState<?SitesFile>,
pendingSitesFileTask: TaskState,
|};

export const PLAN_ID_NEW = '';
Expand Down Expand Up @@ -121,6 +133,14 @@ const defaultValue: NetworkPlanningContext = {
setOutputFiles: empty,
loadOutputFilesTask: emptyTask,
downloadOutputTask: emptyTask,
sitesFile: null,
setSitesFile: empty,
sitesFileTask: emptyTask,
selectedSites: [],
setSelectedSites: empty,
pendingSitesFile: null,
setPendingSitesFile: empty,
pendingSitesFileTask: emptyTask,
};

const context = React.createContext<NetworkPlanningContext>(defaultValue);
Expand All @@ -139,6 +159,10 @@ export type NetworkPlanningContextProviderProps = {|
mapOptions?: MapOptionsState,
setMapOptions?: () => void,
plan?: ?NetworkPlan,
/**
* Only use this for testing
*/
__ref?: {current: ?NetworkPlanningContext},
|};

/**
Expand All @@ -155,6 +179,7 @@ export function NetworkPlanningContextProvider({
pendingTopology: overridePendingTopology = null,
folders = null,
plan = null,
__ref = null,
}: NetworkPlanningContextProviderProps) {
const {history, location} = useRouter();
const setSelectedPlanId = React.useCallback(
Expand Down Expand Up @@ -229,46 +254,73 @@ export function NetworkPlanningContextProvider({
} = useStateWithTaskState<?Array<ANPFileHandle>>(null);
const downloadOutputTask = useTaskState();

const {
obj: sitesFile,
setter: setSitesFile,
taskState: sitesFileTask,
} = useStateWithTaskState<?SitesFile>(null);
const {
obj: pendingSitesFile,
setter: setPendingSitesFile,
taskState: pendingSitesFileTask,
} = useStateWithTaskState<?SitesFile>(null);

const [selectedSites, setSelectedSites] = React.useState<Array<number>>([]);
// Other state objects.
const [_folders, setFolders] = React.useState<?FolderMap>(folders);
const [refreshDate, setRefreshDate] = React.useState(new Date().getTime());
return (
<context.Provider
value={{
selectedPlanId,
setSelectedPlanId,
plan: _plan,
setPlan: _setPlan,
loadPlanTask,
// Use overrides if passed in.
planTopology,
setPlanTopology,
folders: _folders,
setFolders,
// Use overrides if passed in.
mapOptions,
setMapOptions,
refreshDate,
setRefreshDate,
const contextVal = {
selectedPlanId,
setSelectedPlanId,
plan: _plan,
setPlan: _setPlan,
loadPlanTask,
// Use overrides if passed in.
planTopology,
setPlanTopology,
folders: _folders,
setFolders,
// Use overrides if passed in.
mapOptions,
setMapOptions,
refreshDate,
setRefreshDate,

// I/O files
inputFiles,
setInputFiles,
loadInputFilesTask,
outputFiles,
setOutputFiles,
loadOutputFilesTask,
// I/O files
inputFiles,
setInputFiles,
loadInputFilesTask,
outputFiles,
setOutputFiles,
loadOutputFilesTask,

downloadOutputTask,
downloadOutputTask,

// Hidden "internal" fields, that should NOT BE ACCESSED
// by anyone BUT useNetworkPlanningManager.
_pendingTopology,
_setPendingTopology,
_pendingTopologyCount,
_setPendingTopologyCount,
}}>
{children}
</context.Provider>
);
// Hidden "internal" fields, that should NOT BE ACCESSED
// by anyone BUT useNetworkPlanningManager.
_pendingTopology,
_setPendingTopology,
_pendingTopologyCount,
_setPendingTopologyCount,

// sites file
sitesFile,
setSitesFile,
sitesFileTask,
selectedSites,
setSelectedSites,
/**
* pendingSitesFile will be synced to the backend by SitesFileTable
* when changed
*/
pendingSitesFile,
setPendingSitesFile,
pendingSitesFileTask,
};
React.useEffect(() => {
if (__ref != null) {
__ref.current = contextVal;
}
});
return <context.Provider value={contextVal}>{children}</context.Provider>;
}
Loading

0 comments on commit ac57db8

Please sign in to comment.