Skip to content

Commit

Permalink
Add UI to view and fix GeoIp Errors
Browse files Browse the repository at this point in the history
- Add table on director displaying top errors
- Update GeoIPOverrideForm.tsx to include a map to click and drag a marker for faster geolocation
  • Loading branch information
CannonLock committed Jan 14, 2025
1 parent 1c987f6 commit 2333f12
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 21 deletions.
10 changes: 2 additions & 8 deletions web_ui/frontend/app/config/Config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ function Config({ metadata }: { metadata: ParameterMetadataRecord }) {
const { data, mutate, error } = useSWR<ParameterValueRecord | undefined>(
'getConfig',
async () =>
await alertOnError(getConfigJson, 'Could not get config', dispatch)
await alertOnError(
async () => (await getConfig()).json(), 'Could not get config', dispatch)
);

const serverConfig = useMemo(() => {
Expand Down Expand Up @@ -181,11 +182,4 @@ function Config({ metadata }: { metadata: ParameterMetadataRecord }) {
);
}

const getConfigJson = async (): Promise<ParameterValueRecord | undefined> => {
const response = await getConfig();
if (response) {
return await response.json();
}
};

export default Config;
161 changes: 161 additions & 0 deletions web_ui/frontend/app/director/components/GeoIpErrorTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* Table printing out the errors that occurred during the GeoIP lookup.
*/

import { styled } from '@mui/material/styles';
import {
Paper,
Table,
TableCell,
TableContainer,
TableHead,
TableRow,
TableBody,
Button,
Modal,
Typography,
} from '@mui/material';
import { tableCellClasses } from '@mui/material/TableCell';
import useSWR from 'swr';

import { query_raw, VectorResponseData } from '@/components';
import { GeoIPOverride, GeoIPOverrideForm, ParameterValueRecord, submitConfigChange } from '@/components/configuration';
import { alertOnError } from '@/helpers/util';
import { Dispatch, useCallback, useContext, useMemo, useState } from 'react';
import { AlertDispatchContext, AlertReducerAction } from '@/components/AlertProvider';
import { getConfig } from '@/helpers/api';
import LatitudeLongitudePicker from '@/components/LatitudeLongitudePicker';
import ObjectModal from '@/components/configuration/Fields/ObjectField/ObjectModal';
import CircularProgress from '@mui/material/CircularProgress';

const GeoIpErrorTable = () => {

const dispatch = useContext(AlertDispatchContext);
const {data: ipErrors} = useSWR('geoip_errors', getIpErrorRows)

const { data: config, mutate, error, isValidating } = useSWR<ParameterValueRecord | undefined>(
'getConfig',
async () =>
await alertOnError(getOverriddenGeoIps, 'Could not get config', dispatch)
);
const patchedIps = useMemo(() => {
return config?.GeoIPOverrides === undefined ?
[] :
Object.values(config.GeoIPOverrides).map((x: GeoIPOverride) => x.ip)
}, [config])
const [geoIPOverrides, setGeoIPOverrides] = useState<Record<string, GeoIPOverride>>({})

const [open, setOpen] = useState(false)
const [ip, setIp] = useState("")

const onSubmit = useCallback((x: GeoIPOverride) => {
setGeoIPOverrides((p) => {
return {...p, [x.ip]: x}
})
setOpen(false)
}, [])

return (
<>
<Typography variant={"h4"} pb={2}>
Un-located Networks
{isValidating && <CircularProgress size={"24px"} sx={{ml: 1}}/>}
</Typography>
<Paper sx={{overflow: "hidden"}}>
<TableContainer sx={{ maxHeight: 250}}>
<Table stickyHeader sx={{ minWidth: 650 }} size={"small"} aria-label="simple table">
<TableHead>
<TableRow>
<StyledTableCell>Un-located Network</StyledTableCell>
<StyledTableCell align="right">Project</StyledTableCell>
<StyledTableCell align="right">Source</StyledTableCell>
<StyledTableCell align={"right"}># of Errors</StyledTableCell>
<StyledTableCell align="right">Locate</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{ipErrors && ipErrors
.filter((x) => !patchedIps.includes(x.metric?.network))
.sort((a, b) => parseInt(b.value[1]) - parseInt(a.value[1]))
.map((row) => (
<TableRow
key={row.metric?.network}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell>{row.metric?.network}</TableCell>
<TableCell align="right">{row.metric?.proj}</TableCell>
<TableCell align="right">{row.metric?.source}</TableCell>
<TableCell align="right">{parseInt(row.value[1]).toLocaleString()}</TableCell>
<TableCell align="right">
<Button
onClick={() => {
setIp(row.metric?.network)
setOpen(true)
}}
>
{Object.keys(geoIPOverrides).includes(row.metric?.network) ? "Pending" : "Locate"}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{Object.keys(geoIPOverrides).length > 0 && (
<Button
sx={{m:1}}
variant={"outlined"}
onClick={async () => {
const overrides = [...Object.values(config?.GeoIPOverrides || []), ...Object.values(geoIPOverrides)]
const value = await alertOnError(async () => submitConfigChange({GeoIPOverrides: overrides}), 'Could not submit IP patches', dispatch)
if(value !== undefined) {
mutate()
setGeoIPOverrides({})
}
}}
>
Submit IP Patches ({Object.keys(geoIPOverrides).length})
</Button>
)}
</Paper>
<ObjectModal name={"Locate Network IP"} handleClose={() => setOpen(!open)} open={open}>
<GeoIPOverrideForm value={{ip: ip, coordinate: {lat: "37", long: "20"}}} onSubmit={onSubmit} />
</ObjectModal>
</>
)
}

interface GeoUpdateFormProps {
open: boolean;
onClose: () => void;
ip: string;
}

const StyledTableCell = styled(TableCell)(({ theme }) => ({

[`&.${tableCellClasses.head}`]: {
backgroundColor: theme.palette.warning.main
},
}));

const getIpErrorRows = async () => {
const response = await query_raw<VectorResponseData>("last_over_time(pelican_director_geoip_errors[1d])")
return response.data.result
}

const getOverriddenGeoIps = async () => {
let tries = 0
while(tries < 2) {
try {
const response = await getConfig()
const config = await response.json()
return {GeoIPOverrides: config.GeoIPOverrides}
} catch(e) {
tries++
await new Promise(r => setTimeout(r, (10 ** tries) * 500));

}
}
}

export default GeoIpErrorTable;
1 change: 1 addition & 0 deletions web_ui/frontend/app/director/components/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './DirectorCard';
export * from './DirectorCardList';
export * from './NamespaceCard';
export { default as GeoIpErrorTable } from './GeoIpErrorTable';
7 changes: 6 additions & 1 deletion web_ui/frontend/app/director/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import { Box, Grid, Skeleton, Typography } from '@mui/material';
import { useContext, useMemo } from 'react';
import useSWR from 'swr';
import { DirectorCardList } from './components';
import { DirectorCardList, GeoIpErrorTable } from './components';
import { getUser } from '@/helpers/login';
import FederationOverview from '@/components/FederationOverview';
import AuthenticatedContent from '@/components/layout/AuthenticatedContent';
Expand Down Expand Up @@ -123,6 +123,11 @@ export default function Page() {
</Box>
)}
</Grid>
<Grid item xs={12} lg={8} xl={6}>
<AuthenticatedContent>
<GeoIpErrorTable />
</AuthenticatedContent>
</Grid>
<Grid item xs={12} lg={8} xl={6}>
<AuthenticatedContent>
<FederationOverview />
Expand Down
45 changes: 45 additions & 0 deletions web_ui/frontend/components/LatitudeLongitudePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* A form element that allows the user to pick a latitude and longitude
*/
import { DefaultMap } from '@/components/Map';
import { Box } from '@mui/material';

interface LatitudeLongitudePickerProps {
latitude: number;
longitude: number;
setLatitude: (latitude: number) => void;
setLongitude: (longitude: number) => void;
}

const LatitudeLongitudePicker = ({
latitude,
longitude,
setLatitude,
setLongitude,
}: LatitudeLongitudePickerProps) => {
return (
<div>
<Box>
<DefaultMap />
</Box>
<label>
Latitude:
<input
type="number"
value={latitude}
onChange={(e) => setLatitude(parseFloat(e.target.value))}
/>
</label>
<label>
Longitude:
<input
type="number"
value={longitude}
onChange={(e) => setLongitude(parseFloat(e.target.value))}
/>
</label>
</div>
);
}

export default LatitudeLongitudePicker
59 changes: 59 additions & 0 deletions web_ui/frontend/components/Map/UpdateSinglePoint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* A form element that allows the user to pick a latitude and longitude
*/
import { DefaultMap } from '@/components/Map';
import { Marker, NavigationControl } from 'react-map-gl/maplibre';
import { FmdGood } from '@mui/icons-material';
import React, { useCallback } from 'react';
import { LngLat } from 'maplibre-gl';

interface LatitudeLongitudePickerProps {
latitude: number;
longitude: number;
setLatitude: (latitude: number) => void;
setLongitude: (longitude: number) => void;
zoom?: number;
}

const LatitudeLongitudePicker = ({
latitude,
longitude,
setLatitude,
setLongitude,
zoom
}: LatitudeLongitudePickerProps) => {

const updateLatLng = useCallback((lngLat: LngLat) => {
setLatitude(parseFloat(lngLat.lat.toFixed(5)));
setLongitude(parseFloat(lngLat.lng.toFixed(5)));
}, []);

const tempLongitude = Number.isNaN(longitude) ? 0 : longitude
const tempLatitude = Number.isNaN(latitude) ? 0 : latitude

return (
<DefaultMap
initialViewState={{
longitude: tempLongitude,
latitude: tempLatitude,
zoom: zoom || 0,
}}
scrollZoom={false}
style={{ width: '100%', height: '100%' }}
onClick={(e) => updateLatLng(e.lngLat)}
>
<NavigationControl />
<Marker
longitude={tempLongitude}
latitude={tempLatitude}
anchor='bottom'
draggable={true}
onDrag={(e) => updateLatLng(e.lngLat as LngLat)}
>
<FmdGood />
</Marker>
</DefaultMap>
);
}

export default LatitudeLongitudePicker
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { Box, Button, TextField, Typography } from '@mui/material';

import {
Expand All @@ -12,6 +12,8 @@ import {
verifyIpAddress,
verifyLongitude,
} from '@/components/configuration/util';
import { SinglePointMap } from '@/components/Map';
import UpdateSinglePoint from '@/components/Map/UpdateSinglePoint';

const verifyForm = (x: GeoIPOverride) => {
return !(
Expand Down Expand Up @@ -42,8 +44,31 @@ const GeoIPOverrideForm = ({ onSubmit, value }: FormProps<GeoIPOverride>) => {
onSubmit(geoIP);
}, [geoIP]);

const lat = useMemo(() => {
return parseFloat(geoIP?.coordinate?.lat);
}, [geoIP]);

const long = useMemo(() => {
return parseFloat(geoIP?.coordinate?.long);
}, [geoIP]);

const setLatitude = useCallback((latitude: number) => {
setGeoIP((geoIP) => {
return { ...geoIP, coordinate: { ...geoIP?.coordinate, lat: latitude.toString() } }
})
}, []);

const setLongitude = useCallback((longitude: number) => {
setGeoIP((geoIP) => {
return { ...geoIP, coordinate: { ...geoIP?.coordinate, long: longitude.toString() } }
});
}, []);

return (
<>
<Box height={"200px"} width={"400px"} maxWidth={"100%"}>
<UpdateSinglePoint latitude={lat} longitude={long} setLatitude={setLatitude} setLongitude={setLongitude} />
</Box>
<Box my={2}>
<StringField
onChange={(e) => setGeoIP({ ...geoIP, ip: e })}
Expand All @@ -54,22 +79,15 @@ const GeoIPOverrideForm = ({ onSubmit, value }: FormProps<GeoIPOverride>) => {
</Box>
<Box mb={2}>
<StringField
onChange={(e) =>
setGeoIP({ ...geoIP, coordinate: { ...geoIP?.coordinate, lat: e } })
}
onChange={(e) => setLatitude(parseFloat(e))}
name={'Latitude'}
value={geoIP?.coordinate?.lat}
verify={verifyLongitude}
/>
</Box>
<Box mb={2}>
<StringField
onChange={(e) =>
setGeoIP({
...geoIP,
coordinate: { ...geoIP?.coordinate, long: e },
})
}
onChange={(e) => setLongitude(parseFloat(e))}
name={'Longitude'}
value={geoIP?.coordinate?.long}
verify={verifyLongitude}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StringField,
} from '@/components/configuration';
import { verifyIpAddress } from '@/components/configuration/util';
import { DefaultMap, SinglePointMap } from '@/components/Map';

const verifySourceIp = (x: string) => {
const isValidIp =
Expand Down
Loading

0 comments on commit 2333f12

Please sign in to comment.