Compare commits

..

10 Commits

Author SHA1 Message Date
CI
b97a055bf7 chore: release version v1.23.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-11-26 21:05:58 +00:00
Dmitry Popov
663fee6699 feat(Map): Lock systems available to manager/admin roles only (#75)
* feat(Map): Lock systems available to manager/admin roles only

* feat(Map): Fix add system & add acl member select behaviour
2024-11-27 01:05:26 +04:00
CI
33d5f3938b chore: release version v1.22.0 2024-11-26 19:03:01 +00:00
Aleksei Chichenkov
ef6b45d7a1 feat(Map): Rework design of checkboxes in Signatures settings dialog. Rework design of checkboxes in Routes settings dialog. Now signature will deleteing by Delete hotkey was Backspace. Fixed size of group column in signatures list. Instead Updated column will be Added, updated may be turn on in settings. (#76)
Co-authored-by: achichenkov <aleksei.chichenkov@telleqt.ai>
2024-11-26 23:02:30 +04:00
CI
c1ecd3690e chore: release version v1.21.0
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-11-24 15:55:38 +00:00
Aleksei Chichenkov
3250fe1ec6 Merge pull request #74 from wanderer-industries/dessign-issues-2
feat(Map): add new gate design, change EOL placement
2024-11-24 18:55:12 +03:00
achichenkov
48e8cd93b9 feat(Map): add new gate design, change EOL placement 2024-11-24 18:30:40 +03:00
CI
afacbb16b6 chore: release version v1.20.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-11-22 12:42:02 +00:00
Dmitry Popov
dfad127f32 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-11-22 13:41:35 +01:00
Dmitry Popov
300c1b5a18 chore: release version v1.19.3 2024-11-22 13:41:30 +01:00
35 changed files with 1484 additions and 1265 deletions

View File

@@ -2,6 +2,42 @@
<!-- changelog -->
## [v1.23.0](https://github.com/wanderer-industries/wanderer/compare/v1.22.0...v1.23.0) (2024-11-26)
### Features:
* Map: Lock systems available to manager/admin roles only (#75)
* Map: Lock systems available to manager/admin roles only
* Map: Fix add system & add acl member select behaviour
## [v1.22.0](https://github.com/wanderer-industries/wanderer/compare/v1.21.0...v1.22.0) (2024-11-26)
### Features:
* Map: Rework design of checkboxes in Signatures settings dialog. Rework design of checkboxes in Routes settings dialog. Now signature will deleteing by Delete hotkey was Backspace. Fixed size of group column in signatures list. Instead Updated column will be Added, updated may be turn on in settings. (#76)
## [v1.21.0](https://github.com/wanderer-industries/wanderer/compare/v1.20.1...v1.21.0) (2024-11-24)
### Features:
* Map: add new gate design, change EOL placement
## [v1.20.1](https://github.com/wanderer-industries/wanderer/compare/v1.20.0...v1.20.1) (2024-11-22)
## [v1.20.0](https://github.com/wanderer-industries/wanderer/compare/v1.19.3...v1.20.0) (2024-11-22)

View File

@@ -6,6 +6,8 @@ import { PrimeIcons } from 'primereact/api';
import { ContextMenuSystemProps } from '@/hooks/Mapper/components/contexts';
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
export const useContextMenuSystemItems = ({
onDeleteSystem,
@@ -25,6 +27,7 @@ export const useContextMenuSystemItems = ({
const getStatus = useStatusMenu(systems, systemId, onSystemStatus);
const getLabels = useLabelsMenu(systems, systemId, onSystemLabels, onCustomLabelDialog);
const getWaypointMenu = useWaypointMenu(onWaypointSet);
const canLockSystem = useMapCheckPermissions([UserPermission.LOCK_SYSTEM]);
return useMemo(() => {
const system = systemId ? getSystemById(systems, systemId) : undefined;
@@ -58,19 +61,25 @@ export const useContextMenuSystemItems = ({
command: onHubToggle,
},
...(system.locked
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
? canLockSystem
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
: []
: [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
...(canLockSystem
? [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
]
: []),
{ separator: true },
{
label: 'Delete',
@@ -80,6 +89,7 @@ export const useContextMenuSystemItems = ({
]),
];
}, [
canLockSystem,
systems,
systemId,
getTags,

View File

@@ -32,6 +32,7 @@ const INITIAL_DATA: MapData = {
visibleNodes: new Set(),
showKSpaceBG: false,
isThickConnections: false,
userPermissions: {},
};
export interface MapContextProps {

View File

@@ -24,6 +24,10 @@
stroke: #d4f0ff;
}
&.Gate {
stroke: #1c1e15;
}
&.Hovered {
stroke: #4e5d6c;
stroke-width: 2px;
@@ -76,6 +80,11 @@
stroke-width: 6px;
}
}
&.Gate {
stroke: #9aff40;
}
}
.ClickPath {

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import classes from './SolarSystemEdge.module.scss';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, Position, useStore } from 'reactflow';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, getSmoothStepPath, Position, useStore } from 'reactflow';
import { getEdgeParams } from '@/hooks/Mapper/components/map/utils.ts';
import clsx from 'clsx';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
@@ -46,7 +46,9 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const [edgePath, labelX, labelY] = getBezierPath({
const method = isWormhole ? getBezierPath : getSmoothStepPath;
const [edgePath, labelX, labelY] = method({
sourceX: sx - offset.x,
sourceY: sy - offset.y,
sourcePosition: sourcePos,
@@ -54,8 +56,9 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
targetX: tx + offset.x,
targetY: ty + offset.y,
});
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
}, [isThickConnections, sourceNode, targetNode]);
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
if (!sourceNode || !targetNode || !data) {
return null;
@@ -69,6 +72,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.Tick]: isThickConnections,
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
[classes.Hovered]: hovered,
[classes.Gate]: !isWormhole,
})}
d={path}
markerEnd={markerEnd}
@@ -82,6 +86,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
[classes.Gate]: !isWormhole,
})}
d={path}
markerEnd={markerEnd}

View File

@@ -1,12 +1,11 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
import {
RoutesType,
useRouteProvider,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
import { CheckboxChangeEvent } from 'primereact/checkbox';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
interface RoutesSettingsDialog {
visible: boolean;
@@ -38,8 +37,8 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
currentData.current = data;
const handleChangeEvent = useCallback(
(propName: keyof RoutesType) => (event: CheckboxChangeEvent) => {
optionsRef.current = { ...optionsRef.current, [propName]: event.checked };
(propName: keyof RoutesType) => (event: boolean) => {
optionsRef.current = { ...optionsRef.current, [propName]: event };
updateKey(x => x + 1);
},
[],
@@ -71,14 +70,14 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
setVisible(false);
}}
>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-3 p-2.5">
<div className="flex flex-col gap-2 mb-2">
{checkboxes.map(({ label, propName }) => (
<WdCheckbox
<PrettySwitchbox
key={propName}
label={label}
value={optionsRef.current[propName]}
onChange={handleChangeEvent(propName)}
checked={optionsRef.current[propName]}
setChecked={handleChangeEvent(propName)}
/>
))}
</div>

View File

@@ -1,9 +1,9 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useState } from 'react';
import { Button } from 'primereact/button';
import { Checkbox } from 'primereact/checkbox';
import { TabPanel, TabView } from 'primereact/tabview';
import styles from './SystemSignatureSettingsDialog.module.scss';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
export type Setting = { key: string; name: string; value: boolean; isFilter?: boolean };
@@ -41,8 +41,8 @@ export const SystemSignatureSettingsDialog = ({
}, [onSave, settings]);
return (
<Dialog header="System Signatures Settings" visible={true} onHide={onCancel} className="w-full max-w-lg">
<div className="flex flex-col gap-3">
<Dialog header="System Signatures Settings" visible={true} onHide={onCancel} className="w-full max-w-lg h-[500px]">
<div className="flex flex-col gap-3 justify-between h-full">
<div className="flex flex-col gap-2">
<div className={styles.verticalTabsContainer}>
<TabView
@@ -54,16 +54,12 @@ export const SystemSignatureSettingsDialog = ({
<div className="w-full h-full flex flex-col gap-1">
{filterSettings.map(setting => {
return (
<div key={setting.key} className="flex items-center">
<Checkbox
inputId={setting.key}
checked={setting.value}
onChange={() => handleSettingsChange(setting.key)}
/>
<label htmlFor={setting.key} className="ml-2">
{setting.name}
</label>
</div>
<PrettySwitchbox
key={setting.key}
label={setting.name}
checked={setting.value}
setChecked={() => handleSettingsChange(setting.key)}
/>
);
})}
</div>
@@ -72,16 +68,12 @@ export const SystemSignatureSettingsDialog = ({
<div className="w-full h-full flex flex-col gap-1">
{userSettings.map(setting => {
return (
<div key={setting.key} className="flex items-center">
<Checkbox
inputId={setting.key}
checked={setting.value}
onChange={() => handleSettingsChange(setting.key)}
/>
<label htmlFor={setting.key} className="ml-2">
{setting.name}
</label>
</div>
<PrettySwitchbox
key={setting.key}
label={setting.name}
checked={setting.value}
setChecked={() => handleSettingsChange(setting.key)}
/>
);
})}
</div>

View File

@@ -22,10 +22,10 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings_v4_1';
export const SHOW_DESCRIPTION_COLUMN_SETTING = 'show_description_column_setting';
export const SHOW_INSERTED_COLUMN_SETTING = 'show_inserted_column_setting';
export const SHOW_UPDATED_COLUMN_SETTING = 'SHOW_UPDATED_COLUMN_SETTING';
const settings: Setting[] = [
{ key: SHOW_INSERTED_COLUMN_SETTING, name: 'Show Inserted Column', value: false, isFilter: false },
{ key: SHOW_UPDATED_COLUMN_SETTING, name: 'Show Updated Column', value: false, isFilter: false },
{ key: SHOW_DESCRIPTION_COLUMN_SETTING, name: 'Show Description Column', value: false, isFilter: false },
{ key: COSMIC_ANOMALY, name: 'Show Anomalies', value: true, isFilter: true },
{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true, isFilter: true },

View File

@@ -1,5 +1,4 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useClipboard } from '@/hooks/Mapper/hooks/useClipboard';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
@@ -22,10 +21,10 @@ import {
getRowColorByTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers';
import {
renderAddedTimeLeft,
renderDescription,
renderIcon,
renderInfoColumn,
renderInsertedTimeLeft,
renderUpdatedTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import useLocalStorageState from 'use-local-storage-state';
@@ -36,7 +35,7 @@ import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrap
import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog';
import {
SHOW_DESCRIPTION_COLUMN_SETTING,
SHOW_INSERTED_COLUMN_SETTING,
SHOW_UPDATED_COLUMN_SETTING,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures';
type SystemSignaturesSortSettings = {
sortField: string;
@@ -44,7 +43,7 @@ type SystemSignaturesSortSettings = {
};
const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
sortField: 'updated_at',
sortField: 'inserted_at',
sortOrder: -1,
};
@@ -80,11 +79,11 @@ export const SystemSignaturesContent = ({
const tableRef = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(tableRef, 260);
const medium = useMaxWidth(tableRef, 380);
const refData = useRef({ selectable });
refData.current = { selectable };
const tooltipRef = useRef<WdTooltipHandlers>(null);
const { clipboardContent } = useClipboard();
const handleResize = useCallback(() => {
if (tableRef.current) {
const tableWidth = tableRef.current.offsetWidth;
@@ -100,10 +99,7 @@ export const SystemSignaturesContent = ({
[settings],
);
const showInsertedColumn = useMemo(
() => settings.find(s => s.key === SHOW_INSERTED_COLUMN_SETTING)?.value,
[settings],
);
const showUpdatedColumn = useMemo(() => settings.find(s => s.key === SHOW_UPDATED_COLUMN_SETTING)?.value, [settings]);
const filteredSignatures = useMemo(() => {
return signatures
@@ -160,19 +156,26 @@ export const SystemSignaturesContent = ({
[outCommand, systemId],
);
const handleDeleteSelected = useCallback(async () => {
if (selectable) {
return;
}
if (selectedSignatures.length === 0) {
return;
}
const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id);
await handleUpdateSignatures(
signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)),
false,
);
}, [handleUpdateSignatures, selectable, signatures, selectedSignatures]);
const handleDeleteSelected = useCallback(
async (e: KeyboardEvent) => {
if (selectable) {
return;
}
if (selectedSignatures.length === 0) {
return;
}
e.preventDefault();
e.stopPropagation();
const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id);
await handleUpdateSignatures(
signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)),
false,
);
},
[handleUpdateSignatures, selectable, signatures, selectedSignatures],
);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
@@ -201,12 +204,9 @@ export const SystemSignaturesContent = ({
[onSelect, selectable],
);
useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace'], handleDeleteSelected);
useEffect(() => {
if (selectable) {
const handlePaste = async () => {
const clipboardContent = await navigator.clipboard.readText();
if (refData.current.selectable) {
return;
}
@@ -227,7 +227,12 @@ export const SystemSignaturesContent = ({
setParsedSignatures(newSignatures);
setAskUser(true);
}
}, [clipboardContent, selectable]);
};
useHotkey(true, ['a'], handleSelectAll);
useHotkey(true, ['v'], handlePaste);
useHotkey(false, ['Delete'], handleDeleteSelected);
useEffect(() => {
if (!systemId) {
@@ -330,7 +335,7 @@ export const SystemSignaturesContent = ({
return clsx(classes.TableRowCompact, 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200');
}
const dateClass = getRowColorByTimeLeft(row.updated_at ? new Date(row.updated_at) : undefined);
const dateClass = getRowColorByTimeLeft(row.inserted_at ? new Date(row.inserted_at) : undefined);
if (!dateClass) {
return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200');
}
@@ -357,6 +362,7 @@ export const SystemSignaturesContent = ({
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={compact}
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
sortable
></Column>
<Column
@@ -378,26 +384,26 @@ export const SystemSignaturesContent = ({
></Column>
)}
{showInsertedColumn && (
<Column
field="inserted_at"
header="Added"
dataType="date"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderAddedTimeLeft}
sortable
></Column>
{showUpdatedColumn && (
<Column
field="inserted_at"
header="Inserted"
field="updated_at"
header="Updated"
dataType="date"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderInsertedTimeLeft}
body={renderUpdatedTimeLeft}
sortable
></Column>
)}
<Column
field="updated_at"
header="Updated"
dataType="date"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderUpdatedTimeLeft}
sortable
></Column>
{!selectable && (
<Column
bodyClassName="p-0 pl-1 pr-2"

View File

@@ -1,7 +1,7 @@
export * from './renderIcon';
export * from './renderDescription';
export * from './renderName';
export * from './renderInsertedTimeLeft';
export * from './renderAddedTimeLeft';
export * from './renderUpdatedTimeLeft';
export * from './renderLinkedSystem';
export * from './renderInfoColumn';

View File

@@ -1,7 +1,7 @@
import { SystemSignature } from '@/hooks/Mapper/types';
import { TimeLeft } from '@/hooks/Mapper/components/ui-kit';
export const renderInsertedTimeLeft = (row: SystemSignature) => {
export const renderAddedTimeLeft = (row: SystemSignature) => {
return (
<div className="flex w-full items-center">
<TimeLeft cDate={row.inserted_at ? new Date(row.inserted_at) : undefined} />

View File

@@ -82,7 +82,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
}, [cnInfo]);
const [passages, setPassages] = useState<Passage[]>([]);
const [info, setInfo] = useState<ConnectionInfoOutput>(null);
const [info, setInfo] = useState<ConnectionInfoOutput | null>(null);
const loadInfo = useCallback(
async (connection: SolarSystemConnection) => {
@@ -141,7 +141,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
>
<div className={clsx(classes.SidebarContent, '')}>
{/* Connection Info */}
<div className="px-2 pb-3 flex flex-col gap-2">
<div className="px-2 flex flex-col gap-2">
{/* Connection Info Row */}
<InfoDrawer title="Connection" rightSide>
<div className="flex justify-end gap-2 items-center">
@@ -159,18 +159,25 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
</div>
</InfoDrawer>
{/* Connection Info Row */}
{isWormhole && (
<>
<InfoDrawer title="Approximate mass of passages" rightSide>
{kgToTons(approximateMass)}
</InfoDrawer>
<div className="flex justify-between gap-2">
{/*Left column*/}
<div>
{isWormhole && info?.marl_eol_time && (
<InfoDrawer title="Mark EOL Time">
<TimeAgo timestamp={info.marl_eol_time} />
</InfoDrawer>
)}
</div>
<InfoDrawer title="Mark EOL Time" rightSide>
{info?.marl_eol_time ? <TimeAgo timestamp={info.marl_eol_time} /> : ' unknown '}
</InfoDrawer>
</>
)}
{/*Right column*/}
<div>
{isWormhole && (
<InfoDrawer title="Approximate mass of passages" rightSide>
{kgToTons(approximateMass)}
</InfoDrawer>
)}
</div>
</div>
<div className="flex gap-2"></div>
</div>

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react';
export const useHotkey = (isMetaKey: boolean, hotkeys: string[], callback: () => void) => {
export const useHotkey = (isMetaKey: boolean, hotkeys: string[], callback: (e: KeyboardEvent) => void) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((!isMetaKey || event.ctrlKey || event.metaKey) && hotkeys.includes(event.key)) {
@@ -8,14 +8,14 @@ export const useHotkey = (isMetaKey: boolean, hotkeys: string[], callback: () =>
return;
}
event.preventDefault();
callback();
callback(event);
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keydown', handleKeyDown, { capture: true });
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keydown', handleKeyDown, { capture: true });
};
}, [isMetaKey, hotkeys, callback]);
};

View File

@@ -1,6 +1,6 @@
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
import { createContext, Dispatch, ForwardedRef, forwardRef, RefObject, SetStateAction, useContext } from 'react';
import { MapHandlers, MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext } from 'react';
import { MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import useLocalStorageState from 'use-local-storage-state';
@@ -25,6 +25,7 @@ const INITIAL_DATA: MapRootData = {
selectedSystems: [],
selectedConnections: [],
userPermissions: {},
};
export enum InterfaceStoredSettingsProps {

View File

@@ -1,5 +1,6 @@
export * from './useMapInit';
export * from './useMapUpdated';
export * from './useMapCheckPermissions';
export * from './useRoutes';
export * from './useCommandsConnections';
export * from './useCommandsSystems';

View File

@@ -0,0 +1,11 @@
import { useMemo } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
export const useMapCheckPermissions = (permissions: UserPermission[]) => {
const {
data: { userPermissions },
} = useMapRootState();
return useMemo(() => permissions.every(x => userPermissions[x]), [permissions, userPermissions]);
};

View File

@@ -19,6 +19,7 @@ export const useMapInit = () => {
user_characters,
present_characters,
hubs,
user_permissions,
}: CommandInit) => {
const updateData: Partial<MapRootData> = {};
@@ -51,6 +52,10 @@ export const useMapInit = () => {
updateData.connections = connections;
}
if (user_permissions) {
updateData.userPermissions = user_permissions;
}
if (hubs) {
updateData.hubs = hubs;
}

View File

@@ -6,3 +6,4 @@ export * from './system';
export * from './mapUnionTypes';
export * from './signatures';
export * from './connectionPassages';
export * from './permissions';

View File

@@ -4,6 +4,7 @@ import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { Kill } from '@/hooks/Mapper/types/kills.ts';
import { UserPermissions } from '@/hooks/Mapper/types';
export enum Commands {
init = 'init',
@@ -58,7 +59,7 @@ export type CommandInit = {
characters: CharacterTypeRaw[];
present_characters: string[];
user_characters: string[];
user_permissions: any;
user_permissions: UserPermissions;
hubs: string[];
routes: RoutesList;
reset?: boolean;

View File

@@ -4,6 +4,7 @@ import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
import { SolarSystemRawType } from '@/hooks/Mapper/types/system.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { UserPermissions } from '@/hooks/Mapper/types';
export type MapUnionTypes = {
wormholesData: Record<string, WormholeDataRaw>;
@@ -17,4 +18,5 @@ export type MapUnionTypes = {
routes?: RoutesList;
kills: Record<number, number>;
connections: SolarSystemConnection[];
userPermissions: Partial<UserPermissions>;
};

View File

@@ -0,0 +1,19 @@
export enum UserPermission {
ADMIN_MAP = 'admin_map',
MANAGE_MAP = 'manage_map',
VIEW_SYSTEM = 'view_system',
VIEW_CHARACTER = 'view_character',
VIEW_CONNECTION = 'view_connection',
ADD_SYSTEM = 'add_system',
ADD_CONNECTION = 'add_connection',
UPDATE_SYSTEM = 'update_system',
TRACK_CHARACTER = 'track_character',
DELETE_CONNECTION = 'delete_connection',
DELETE_SYSTEM = 'delete_system',
LOCK_SYSTEM = 'lock_system',
ADD_ACL = 'add_acl',
DELETE_ACL = 'delete_acl',
DELETE_MAP = 'delete_map',
}
export type UserPermissions = Record<UserPermission, boolean>;

View File

@@ -61,12 +61,18 @@ defmodule WandererApp.Character do
end)
end
def get_character_state(character_id) do
def get_character_state(character_id, init_if_empty? \\ true) do
case Cachex.get(:character_state_cache, character_id) do
{:ok, nil} ->
character_state = WandererApp.Character.Tracker.init(character_id: character_id)
Cachex.put(:character_state_cache, character_id, character_state)
{:ok, character_state}
case init_if_empty? do
true ->
character_state = WandererApp.Character.Tracker.init(character_id: character_id)
Cachex.put(:character_state_cache, character_id, character_state)
{:ok, character_state}
_ ->
{:ok, nil}
end
{:ok, character_state} ->
{:ok, character_state}

View File

@@ -83,22 +83,28 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def stop_tracking(%__MODULE__{} = state, character_id) do
{:ok, %{start_time: start_time}} = WandererApp.Character.get_character_state(character_id)
{:ok, character_state} = WandererApp.Character.get_character_state(character_id, false)
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{duration: duration})
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
case character_state do
nil ->
state
WandererApp.Cache.delete("character:#{character_id}:location_started")
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
WandererApp.Character.delete_character_state(character_id)
%{start_time: start_time} ->
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{duration: duration})
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
tracked_characters = state.characters |> Enum.reject(fn c_id -> c_id == character_id end)
WandererApp.Cache.delete("character:#{character_id}:location_started")
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
WandererApp.Character.delete_character_state(character_id)
WandererApp.Cache.insert("tracked_characters", tracked_characters)
tracked_characters = state.characters |> Enum.reject(fn c_id -> c_id == character_id end)
%{state | characters: tracked_characters}
WandererApp.Cache.insert("tracked_characters", tracked_characters)
%{state | characters: tracked_characters}
end
end
def update_track_settings(
@@ -429,8 +435,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def handle_info({:stop_track, character_id}, state) do
@logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
stop_tracking(state, character_id)
WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking")
|> case do
false ->
WandererApp.Cache.insert("character:#{character_id}:is_stop_tracking", true)
Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
state = state |> stop_tracking(character_id)
WandererApp.Cache.delete("character:#{character_id}:is_stop_tracking")
state
_ ->
state
end
end
def handle_info(_event, state),

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,180 @@
defmodule WandererApp.Map.Server.AclsImpl do
@moduledoc false
require Logger
alias WandererApp.Map.Server.Impl
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
def handle_map_acl_updated(%{map_id: map_id, map: old_map} = state, added_acls, removed_acls) do
{:ok, map} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
)
track_acls(added_acls)
result =
(added_acls ++ removed_acls)
|> Task.async_stream(
fn acl_id ->
update_acl(acl_id)
end,
max_concurrency: 10,
timeout: :timer.seconds(15)
)
|> Enum.reduce(
%{
eve_alliance_ids: [],
eve_character_ids: [],
eve_corporation_ids: []
},
fn result, acc ->
case result do
{:ok, val} ->
{:ok,
%{
eve_alliance_ids: eve_alliance_ids,
eve_character_ids: eve_character_ids,
eve_corporation_ids: eve_corporation_ids
}} = val
%{
acc
| eve_alliance_ids: eve_alliance_ids ++ acc.eve_alliance_ids,
eve_character_ids: eve_character_ids ++ acc.eve_character_ids,
eve_corporation_ids: eve_corporation_ids ++ acc.eve_corporation_ids
}
error ->
Logger.error("Failed to update map #{map_id} acl: #{inspect(error, pretty: true)}")
acc
end
end
)
map_update = %{acls: map.acls, scope: map.scope}
WandererApp.Map.update_map(map_id, map_update)
broadcast_acl_updates({:ok, result}, map_id)
%{state | map: Map.merge(old_map, map_update)}
end
def handle_acl_updated(map_id, acl_id) do
{:ok, map} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
)
if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
WandererApp.Map.update_map(map_id, %{acls: map.acls})
:ok =
acl_id
|> update_acl()
|> broadcast_acl_updates(map_id)
end
end
def track_acls([]), do: :ok
def track_acls([acl_id | rest]) do
track_acl(acl_id)
track_acls(rest)
end
defp track_acl(acl_id),
do: @pubsub_client.subscribe(WandererApp.PubSub, "acls:#{acl_id}")
defp broadcast_acl_updates(
{:ok,
%{
eve_character_ids: eve_character_ids,
eve_corporation_ids: eve_corporation_ids,
eve_alliance_ids: eve_alliance_ids
}},
map_id
) do
eve_character_ids
|> Enum.uniq()
|> Enum.each(fn eve_character_id ->
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{eve_character_id}",
:update_permissions
)
end)
eve_corporation_ids
|> Enum.uniq()
|> Enum.each(fn eve_corporation_id ->
@pubsub_client.broadcast(
WandererApp.PubSub,
"corporation:#{eve_corporation_id}",
:update_permissions
)
end)
eve_alliance_ids
|> Enum.uniq()
|> Enum.each(fn eve_alliance_id ->
@pubsub_client.broadcast(
WandererApp.PubSub,
"alliance:#{eve_alliance_id}",
:update_permissions
)
end)
character_ids =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:characters, [])
WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids)
:ok
end
defp broadcast_acl_updates(_, _map_id), do: :ok
defp update_acl(acl_id) do
{:ok, %{owner: owner, members: members}} =
WandererApp.AccessListRepo.get(acl_id, [:owner, :members])
result =
members
|> Enum.reduce(
%{eve_character_ids: [owner.eve_id], eve_corporation_ids: [], eve_alliance_ids: []},
fn member, acc ->
case member do
%{eve_character_id: eve_character_id} when not is_nil(eve_character_id) ->
acc
|> Map.put(:eve_character_ids, [eve_character_id | acc.eve_character_ids])
%{eve_corporation_id: eve_corporation_id} when not is_nil(eve_corporation_id) ->
acc
|> Map.put(:eve_corporation_ids, [eve_corporation_id | acc.eve_corporation_ids])
%{eve_alliance_id: eve_alliance_id} when not is_nil(eve_alliance_id) ->
acc
|> Map.put(:eve_alliance_ids, [eve_alliance_id | acc.eve_alliance_ids])
_ ->
acc
end
end
)
{:ok, result}
end
end

View File

@@ -0,0 +1,459 @@
defmodule WandererApp.Map.Server.CharactersImpl do
@moduledoc false
require Logger
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
def get_characters(%{map_id: map_id} = _state),
do: {:ok, map_id |> WandererApp.Map.list_characters()}
def add_character(%{map_id: map_id} = state, %{id: character_id} = character, track_character) do
Task.start_link(fn ->
with :ok <- map_id |> WandererApp.Map.add_character(character),
{:ok, _} <-
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: track_character
}),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
Impl.broadcast!(map_id, :character_added, character)
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
:ok
else
_error ->
{:ok, character} = WandererApp.Character.get_character(character_id)
Impl.broadcast!(map_id, :character_added, character)
:ok
end
end)
state
end
def remove_character(map_id, character_id) do
Task.start_link(fn ->
with :ok <- WandererApp.Map.remove_character(map_id, character_id),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
Impl.broadcast!(map_id, :character_removed, character)
:telemetry.execute([:wanderer_app, :map, :character, :removed], %{count: 1})
:ok
else
{:error, _error} ->
:ok
end
end)
end
def update_tracked_characters(map_id) do
Task.start_link(fn ->
{:ok, map_tracked_character_ids} =
map_id
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all()
|> case do
{:ok, settings} -> {:ok, settings |> Enum.map(&Map.get(&1, :character_id))}
_ -> {:ok, []}
end
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
map_active_tracked_characters =
map_tracked_character_ids
|> Enum.filter(fn character -> character in tracked_characters end)
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
:ok
end)
end
def untrack_characters(map_id, character_ids),
do:
character_ids
|> Enum.each(fn character_id ->
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: false
})
end)
def cleanup_characters(map_id, owner_id) do
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
"map_#{map_id}:invalidate_character_ids",
[]
)
invalidate_character_ids
|> Task.async_stream(
fn character_id ->
character_id
|> WandererApp.Character.get_character()
|> case do
{:ok, character} ->
acls =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:acls, [])
[character_permissions] =
WandererApp.Permissions.check_characters_access([character], acls)
map_permissions =
WandererApp.Permissions.get_map_permissions(
character_permissions,
owner_id,
[character_id]
)
case map_permissions do
%{view_system: false} ->
{:remove_character, character_id}
%{track_character: false} ->
{:remove_character, character_id}
_ ->
:ok
end
_ ->
:ok
end
end,
timeout: :timer.seconds(60),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, {:remove_character, character_id}} ->
remove_and_untrack_characters(map_id, [character_id])
:ok
{:ok, _result} ->
:ok
{:error, reason} ->
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
end)
WandererApp.Cache.insert(
"map_#{map_id}:invalidate_character_ids",
[]
)
end
defp remove_and_untrack_characters(map_id, character_ids) do
Logger.debug(fn ->
"Map #{map_id} - remove and untrack characters #{inspect(character_ids)}"
end)
map_id
|> untrack_characters(character_ids)
map_id
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(character_ids)
|> case do
{:ok, settings} ->
settings
|> Enum.each(fn s ->
WandererApp.MapCharacterSettingsRepo.untrack(s)
remove_character(map_id, s.character_id)
end)
_ ->
:ok
end
end
def track_characters(_map_id, []), do: :ok
def track_characters(map_id, [character_id | rest]) do
track_character(map_id, character_id)
track_characters(map_id, rest)
end
def update_characters(%{map_id: map_id} = state) do
WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", [])
|> Enum.map(fn character_id ->
Task.start_link(fn ->
character_updates =
maybe_update_online(map_id, character_id) ++
maybe_update_location(map_id, character_id) ++
maybe_update_ship(map_id, character_id) ++
maybe_update_alliance(map_id, character_id) ++
maybe_update_corporation(map_id, character_id)
character_updates
|> Enum.filter(fn update -> update != :skip end)
|> Enum.map(fn update ->
update
|> case do
{:character_location, location_info, old_location_info} ->
update_location(
character_id,
location_info,
old_location_info,
state
)
:broadcast
{:character_ship, _info} ->
:broadcast
{:character_online, _info} ->
:broadcast
{:character_alliance, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids]
end
)
:broadcast
{:character_corporation, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids]
end
)
:broadcast
_ ->
:skip
end
end)
|> Enum.filter(fn update -> update != :skip end)
|> Enum.uniq()
|> Enum.each(fn update ->
case update do
:broadcast ->
update_character(map_id, character_id)
_ ->
:ok
end
end)
:ok
end)
end)
end
defp update_character(map_id, character_id) do
{:ok, character} = WandererApp.Character.get_character(character_id)
Impl.broadcast!(map_id, :character_updated, character)
end
defp update_location(
character_id,
location,
old_location,
%{map: map, map_id: map_id, rtree_name: rtree_name, map_opts: map_opts} = _state
) do
case is_nil(old_location.solar_system_id) and
ConnectionsImpl.can_add_location(map.scope, location.solar_system_id) do
true ->
:ok = SystemsImpl.maybe_add_system(map_id, location, nil, rtree_name, map_opts)
_ ->
ConnectionsImpl.is_connection_valid(
map.scope,
old_location.solar_system_id,
location.solar_system_id
)
|> case do
true ->
:ok =
SystemsImpl.maybe_add_system(map_id, location, old_location, rtree_name, map_opts)
:ok =
SystemsImpl.maybe_add_system(map_id, old_location, location, rtree_name, map_opts)
:ok =
ConnectionsImpl.maybe_add_connection(map_id, location, old_location, character_id)
_ ->
:ok
end
end
end
defp track_character(map_id, character_id),
do:
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: true,
track_online: true,
track_location: true,
track_ship: true
})
defp maybe_update_online(map_id, character_id) do
with {:ok, old_online} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:online"),
{:ok, %{online: online}} <-
WandererApp.Character.get_character(character_id) do
case old_online != online do
true ->
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:online",
online
)
[{:character_online, %{online: online}}]
_ ->
[:skip]
end
else
error ->
Logger.error("Failed to update online: #{inspect(error, pretty: true)}")
[:skip]
end
end
defp maybe_update_ship(map_id, character_id) do
with {:ok, old_ship_type_id} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_type_id"),
{:ok, old_ship_name} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_name"),
{:ok, %{ship: ship_type_id, ship_name: ship_name}} <-
WandererApp.Character.get_character(character_id) do
case old_ship_type_id != ship_type_id or
old_ship_name != ship_name do
true ->
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:ship_type_id",
ship_type_id
)
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:ship_name",
ship_name
)
[{:character_ship, %{ship: ship_type_id, ship_name: ship_name}}]
_ ->
[:skip]
end
else
error ->
Logger.error("Failed to update ship: #{inspect(error, pretty: true)}")
[:skip]
end
end
defp maybe_update_location(map_id, character_id) do
WandererApp.Cache.lookup!(
"character:#{character_id}:location_started",
false
)
|> case do
true ->
{:ok, old_solar_system_id} =
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id")
{:ok, %{solar_system_id: solar_system_id}} =
WandererApp.Character.get_character(character_id)
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:solar_system_id",
solar_system_id
)
case solar_system_id != old_solar_system_id do
true ->
[
{:character_location, %{solar_system_id: solar_system_id},
%{solar_system_id: old_solar_system_id}}
]
_ ->
[:skip]
end
false ->
{:ok, old_solar_system_id} =
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id")
{:ok, %{solar_system_id: solar_system_id} = _character} =
WandererApp.Character.get_character(character_id)
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:solar_system_id",
solar_system_id
)
if is_nil(old_solar_system_id) or solar_system_id != old_solar_system_id do
[
{:character_location, %{solar_system_id: solar_system_id}, %{solar_system_id: nil}}
]
else
[:skip]
end
end
end
defp maybe_update_alliance(map_id, character_id) do
with {:ok, old_alliance_id} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:alliance_id"),
{:ok, %{alliance_id: alliance_id}} <-
WandererApp.Character.get_character(character_id) do
case old_alliance_id != alliance_id do
true ->
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:alliance_id",
alliance_id
)
[{:character_alliance, %{alliance_id: alliance_id}}]
_ ->
[:skip]
end
else
error ->
Logger.error("Failed to update alliance: #{inspect(error, pretty: true)}")
[:skip]
end
end
defp maybe_update_corporation(map_id, character_id) do
with {:ok, old_corporation_id} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:corporation_id"),
{:ok, %{corporation_id: corporation_id}} <-
WandererApp.Character.get_character(character_id) do
case old_corporation_id != corporation_id do
true ->
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:corporation_id",
corporation_id
)
[{:character_corporation, %{corporation_id: corporation_id}}]
_ ->
[:skip]
end
else
error ->
Logger.error("Failed to update corporation: #{inspect(error, pretty: true)}")
[:skip]
end
end
end

View File

@@ -1,7 +1,6 @@
defmodule WandererApp.Map.Server.ConnectionsImpl do
@moduledoc """
Holds state for a map and exposes an interface to managing the map instance
"""
@moduledoc false
require Logger
alias WandererApp.Map.Server.Impl
@@ -504,7 +503,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
state
else
{:error, error} ->
@logger.error("Failed to update connection: #{inspect(error, pretty: true)}")
Logger.error("Failed to update connection: #{inspect(error, pretty: true)}")
state
end

View File

@@ -0,0 +1,493 @@
defmodule WandererApp.Map.Server.SystemsImpl do
@moduledoc false
require Logger
alias WandererApp.Map.Server.{Impl}
@ddrt Application.compile_env(:wanderer_app, :ddrt)
@system_auto_expire_minutes 15
@system_inactive_timeout :timer.minutes(15)
def init_last_activity_cache(map_id, systems_last_activity) do
systems_last_activity
|> Enum.each(fn {system_id, last_activity} ->
WandererApp.Cache.put(
"map_#{map_id}:system_#{system_id}:last_activity",
last_activity,
ttl: @system_inactive_timeout
)
end)
end
def init_map_systems(state, [] = _systems), do: state
def init_map_systems(%{map_id: map_id, rtree_name: rtree_name} = state, systems) do
systems
|> Enum.each(fn %{id: system_id, solar_system_id: solar_system_id} = system ->
@ddrt.insert(
{solar_system_id, WandererApp.Map.PositionCalculator.get_system_bounding_rect(system)},
rtree_name
)
WandererApp.Cache.put(
"map_#{map_id}:system_#{system_id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
end)
state
end
def add_system(
%{map_id: map_id} = state,
%{
solar_system_id: solar_system_id
} = system_info,
user_id,
character_id
) do
case map_id |> WandererApp.Map.check_location(%{solar_system_id: solar_system_id}) do
{:ok, _location} ->
state |> _add_system(system_info, user_id, character_id)
{:error, :already_exists} ->
state
end
end
def cleanup_systems(%{map_id: map_id} = state) do
expired_systems =
map_id
|> WandererApp.Map.list_systems!()
|> Enum.filter(fn %{
id: system_id,
visible: system_visible,
locked: system_locked,
solar_system_id: solar_system_id
} = _system ->
last_updated_time =
WandererApp.Cache.get("map_#{map_id}:system_#{system_id}:last_activity")
if system_visible and not system_locked and
(is_nil(last_updated_time) or
DateTime.diff(DateTime.utc_now(), last_updated_time, :minute) >=
@system_auto_expire_minutes) do
no_active_connections? =
map_id
|> WandererApp.Map.find_connections(solar_system_id)
|> Enum.empty?()
no_active_characters? =
map_id |> WandererApp.Map.get_system_characters(solar_system_id) |> Enum.empty?()
no_active_connections? and no_active_characters?
else
false
end
end)
|> Enum.map(& &1.solar_system_id)
case expired_systems |> Enum.empty?() do
false ->
state |> delete_systems(expired_systems, nil, nil)
_ ->
state
end
end
def update_system_name(
state,
update
),
do: state |> update_system(:update_name, [:name], update)
def update_system_description(
state,
update
),
do: state |> update_system(:update_description, [:description], update)
def update_system_status(
state,
update
),
do: state |> update_system(:update_status, [:status], update)
def update_system_tag(
state,
update
),
do: state |> update_system(:update_tag, [:tag], update)
def update_system_locked(
state,
update
),
do: state |> update_system(:update_locked, [:locked], update)
def update_system_labels(
state,
update
),
do: state |> update_system(:update_labels, [:labels], update)
def update_system_position(
%{rtree_name: rtree_name} = state,
update
),
do:
state
|> update_system(
:update_position,
[:position_x, :position_y],
update,
fn updated_system ->
@ddrt.update(
updated_system.solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(updated_system),
rtree_name
)
end
)
def add_hub(
%{map_id: map_id} = state,
hub_info
) do
with :ok <- WandererApp.Map.add_hub(map_id, hub_info),
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs(),
{:ok, _} <-
WandererApp.MapRepo.update_hubs(map_id, hubs) do
Impl.broadcast!(map_id, :update_map, %{hubs: hubs})
state
else
error ->
Logger.error("Failed to add hub: #{inspect(error, pretty: true)}")
state
end
end
def remove_hub(
%{map_id: map_id} = state,
hub_info
) do
with :ok <- WandererApp.Map.remove_hub(map_id, hub_info),
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs(),
{:ok, _} <-
WandererApp.MapRepo.update_hubs(map_id, hubs) do
Impl.broadcast!(map_id, :update_map, %{hubs: hubs})
state
else
error ->
Logger.error("Failed to remove hub: #{inspect(error, pretty: true)}")
state
end
end
def delete_systems(
%{map_id: map_id, rtree_name: rtree_name} = state,
removed_ids,
user_id,
character_id
) do
connections_to_remove =
removed_ids
|> Enum.map(fn solar_system_id ->
WandererApp.Map.find_connections(map_id, solar_system_id)
end)
|> List.flatten()
|> Enum.uniq_by(& &1.id)
:ok = WandererApp.Map.remove_connections(map_id, connections_to_remove)
:ok = WandererApp.Map.remove_systems(map_id, removed_ids)
removed_ids
|> Enum.each(fn solar_system_id ->
map_id
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
|> case do
{:ok, _} ->
:ok
{:error, error} ->
Logger.error("Failed to remove system from map: #{inspect(error, pretty: true)}")
:ok
end
end)
connections_to_remove
|> Enum.each(fn connection ->
Logger.debug(fn -> "Removing connection from map: #{inspect(connection)}" end)
WandererApp.MapConnectionRepo.destroy(map_id, connection)
end)
@ddrt.delete(removed_ids, rtree_name)
Impl.broadcast!(map_id, :remove_connections, connections_to_remove)
Impl.broadcast!(map_id, :systems_removed, removed_ids)
case not is_nil(user_id) do
true ->
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:systems_removed, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_ids: removed_ids
})
:telemetry.execute(
[:wanderer_app, :map, :systems, :remove],
%{count: removed_ids |> Enum.count()}
)
:ok
_ ->
:ok
end
state
end
def maybe_add_system(map_id, location, old_location, rtree_name, map_opts)
when not is_nil(location) do
case WandererApp.Map.check_location(map_id, location) do
{:ok, location} ->
{:ok, position} = calc_new_system_position(map_id, old_location, rtree_name, map_opts)
case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(
map_id,
location.solar_system_id
) do
{:ok, existing_system} when not is_nil(existing_system) ->
{:ok, updated_system} =
existing_system
|> WandererApp.MapSystemRepo.update_position!(%{
position_x: position.x,
position_y: position.y
})
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|> WandererApp.MapSystemRepo.update_visible!(%{visible: true})
|> WandererApp.MapSystemRepo.cleanup_tags()
@ddrt.insert(
{existing_system.solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
position_x: position.x,
position_y: position.y
})},
rtree_name
)
WandererApp.Cache.put(
"map_#{map_id}:system_#{updated_system.id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
WandererApp.Map.add_system(map_id, updated_system)
Impl.broadcast!(map_id, :add_system, updated_system)
:ok
_ ->
{:ok, solar_system_info} =
WandererApp.CachedInfo.get_system_static_info(location.solar_system_id)
WandererApp.MapSystemRepo.create(%{
map_id: map_id,
solar_system_id: location.solar_system_id,
name: solar_system_info.solar_system_name,
position_x: position.x,
position_y: position.y
})
|> case do
{:ok, new_system} ->
@ddrt.insert(
{new_system.solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(new_system)},
rtree_name
)
WandererApp.Cache.put(
"map_#{map_id}:system_#{new_system.id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
WandererApp.Map.add_system(map_id, new_system)
Impl.broadcast!(map_id, :add_system, new_system)
:ok
error ->
Logger.warning("Failed to create system: #{inspect(error, pretty: true)}")
:ok
end
end
error ->
Logger.debug("Skip adding system: #{inspect(error, pretty: true)}")
:ok
end
end
def maybe_add_system(_map_id, _location, _old_location, _rtree_name, _map_opts), do: :ok
defp _add_system(
%{map_id: map_id, map_opts: map_opts, rtree_name: rtree_name} = state,
%{
solar_system_id: solar_system_id,
coordinates: coordinates
} = system_info,
user_id,
character_id
) do
%{"x" => x, "y" => y} =
coordinates
|> case do
%{"x" => x, "y" => y} ->
%{"x" => x, "y" => y}
_ ->
%{x: x, y: y} =
WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts)
%{"x" => x, "y" => y}
end
{:ok, system} =
case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do
{:ok, existing_system} when not is_nil(existing_system) ->
use_old_coordinates = Map.get(system_info, :use_old_coordinates, false)
if use_old_coordinates do
@ddrt.insert(
{solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
position_x: existing_system.position_x,
position_y: existing_system.position_y
})},
rtree_name
)
existing_system
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
else
@ddrt.insert(
{solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
position_x: x,
position_y: y
})},
rtree_name
)
existing_system
|> WandererApp.MapSystemRepo.update_position!(%{position_x: x, position_y: y})
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|> WandererApp.MapSystemRepo.cleanup_tags!()
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
end
_ ->
{:ok, solar_system_info} =
WandererApp.CachedInfo.get_system_static_info(solar_system_id)
@ddrt.insert(
{solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
position_x: x,
position_y: y
})},
rtree_name
)
WandererApp.MapSystemRepo.create(%{
map_id: map_id,
solar_system_id: solar_system_id,
name: solar_system_info.solar_system_name,
position_x: x,
position_y: y
})
end
:ok = map_id |> WandererApp.Map.add_system(system)
WandererApp.Cache.put(
"map_#{map_id}:system_#{system.id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
Impl.broadcast!(map_id, :add_system, system)
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:system_added, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: solar_system_id
})
state
end
defp calc_new_system_position(map_id, old_location, rtree_name, opts),
do:
{:ok,
map_id
|> WandererApp.Map.find_system_by_location(old_location)
|> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name, opts)}
defp update_system(
%{map_id: map_id} = state,
update_method,
attributes,
update,
callback_fn \\ nil
) do
with :ok <- WandererApp.Map.update_system_by_solar_system_id(map_id, update),
{:ok, system} <-
WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(
map_id,
update.solar_system_id
),
{:ok, update_map} <- Impl.get_update_map(update, attributes) do
{:ok, updated_system} =
apply(WandererApp.MapSystemRepo, update_method, [
system,
update_map
])
if not is_nil(callback_fn) do
callback_fn.(updated_system)
end
update_map_system_last_activity(map_id, updated_system)
state
else
error ->
Logger.error("Fail ed to update system: #{inspect(error, pretty: true)}")
state
end
end
defp update_map_system_last_activity(
map_id,
updated_system
) do
WandererApp.Cache.put(
"map_#{map_id}:system_#{updated_system.id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
Impl.broadcast!(map_id, :update_system, updated_system)
end
end

View File

@@ -15,6 +15,8 @@ defmodule WandererApp.Permissions do
@add_acl 1024
@delete_acl 2048
@delete_map 4096
@manage_map 8192
@admin_map 16384
@viewer_role [@view_system, @view_character, @view_connection]
@member_role @viewer_role ++
@@ -24,11 +26,10 @@ defmodule WandererApp.Permissions do
@update_system,
@track_character,
@delete_connection,
@delete_system,
@lock_system
@delete_system
]
@manager_role @member_role
@admin_role @manager_role ++ [@add_acl, @delete_acl, @delete_map]
@manager_role @member_role ++ [@lock_system, @manage_map]
@admin_role @manager_role ++ [@add_acl, @delete_acl, @delete_map, @admin_map]
@viewer_role_mask @viewer_role |> Enum.reduce(0, fn x, acc -> x ||| acc end)
@member_role_mask @member_role |> Enum.reduce(0, fn x, acc -> x ||| acc end)
@@ -72,6 +73,8 @@ defmodule WandererApp.Permissions do
def get_permissions(user_permissions) do
%{
admin_map: check_permission(user_permissions, @admin_map),
manage_map: check_permission(user_permissions, @manage_map),
view_system: check_permission(user_permissions, @view_system),
view_character: check_permission(user_permissions, @view_character),
view_connection: check_permission(user_permissions, @view_connection),

View File

@@ -222,7 +222,7 @@ defmodule WandererAppWeb.AccessListsLive do
def handle_event(
"add_members",
%{"member_id" => member_id} = _params,
%{"member_id" => [member_id]} = _params,
%{assigns: assigns} = socket
)
when is_binary(member_id) and member_id != "" do

View File

@@ -228,6 +228,7 @@
available_option_class="w-full"
debounce={250}
update_min_len={3}
mode={:tags}
options={@member_search_options}
placeholder="Search a character/corporation/alliance"
>

View File

@@ -74,23 +74,20 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
}
} = socket
) do
{:ok, character_settings} =
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
{:ok, settings} -> {:ok, settings}
_ -> {:ok, []}
end
{:noreply,
socket
|> assign(
show_tracking?: true,
character_settings: character_settings
)
|> assign(show_tracking?: true)
|> assign_async(:characters, fn ->
{:ok, map} =
map_id
|> WandererApp.MapRepo.get([:acls])
{:ok, character_settings} =
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
{:ok, settings} -> {:ok, settings}
_ -> {:ok, []}
end
map
|> WandererApp.Maps.load_characters(
character_settings,
@@ -122,12 +119,17 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
%{
assigns: %{
map_id: map_id,
character_settings: character_settings,
current_user: current_user,
only_tracked_characters: only_tracked_characters
}
} = socket
) do
{:ok, character_settings} =
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
{:ok, settings} -> {:ok, settings}
_ -> {:ok, []}
end
socket =
case character_settings |> Enum.find(&(&1.character_id == character_id)) do
nil ->
@@ -202,7 +204,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
socket
|> assign(user_characters: user_character_eve_ids)
|> assign(has_tracked_characters?: has_tracked_characters?(user_character_eve_ids))
|> assign(character_settings: character_settings)
|> assign_async(:characters, fn ->
{:ok, %{characters: characters}}
end)

View File

@@ -66,7 +66,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
def handle_ui_event(
"add_system",
%{"system_id" => solar_system_id} = _event,
%{"system_id" => [solar_system_id]} = _event,
%{
assigns:
%{
@@ -217,7 +217,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
current_user: current_user,
tracked_character_ids: tracked_character_ids,
has_tracked_characters?: true,
user_permissions: %{update_system: true}
user_permissions: %{update_system: true} = user_permissions
}
} =
socket
@@ -244,23 +244,25 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
_ -> :none
end
apply(WandererApp.Map.Server, method_atom, [
map_id,
%{
solar_system_id: "#{solar_system_id}" |> String.to_integer()
}
|> Map.put_new(key_atom, value)
])
if can_update_system?(key_atom, user_permissions) do
apply(WandererApp.Map.Server, method_atom, [
map_id,
%{
solar_system_id: "#{solar_system_id}" |> String.to_integer()
}
|> Map.put_new(key_atom, value)
])
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:system_updated, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: "#{solar_system_id}" |> String.to_integer(),
key: key_atom,
value: value
})
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:system_updated, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: "#{solar_system_id}" |> String.to_integer(),
key: key_atom,
value: value
})
end
{:noreply, socket}
end
@@ -305,6 +307,9 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
def handle_ui_event(event, body, socket),
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
defp can_update_system?(:locked, %{lock_system: false} = _user_permissions), do: false
defp can_update_system?(_key, _user_permissions), do: true
defp update_system_positions(_map_id, []), do: :ok
defp update_system_positions(map_id, [position | rest]) do

View File

@@ -46,6 +46,7 @@
update_min_len={2}
available_option_class="w-full text-sm"
debounce={200}
mode={:tags}
>
<:option :let={option}>
<div class="gap-1 w-full flex flex-align-center p-autocomplete-item text-sm">
@@ -96,7 +97,7 @@
on_cancel={JS.push("hide_tracking")}
>
<.async_result :let={characters} assign={@characters}>
<:loading>Loading...</:loading>
<:loading><span class="loading loading-dots loading-xs" />.</:loading>
<:failed :let={reason}><%= reason %></:failed>
<.table
@@ -104,7 +105,6 @@
id="characters-tracking-table"
class="h-[400px] !overflow-y-auto"
rows={characters}
>
<:col :let={character} label="Tracked">
<label class="flex items-center gap-3">

View File

@@ -2,7 +2,7 @@ defmodule WandererApp.MixProject do
use Mix.Project
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.20.0"
@version "1.23.0"
def project do
[