diff --git a/client/src/App.css b/client/src/App.css index b79dbe8..9f5c918 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -21,6 +21,7 @@ div { .Resizer { width: 0.5rem; + background-color: #ccc; border: 1px solid black; } @@ -36,6 +37,8 @@ div { .Identities { display: flex; + padding: 0.25rem; + gap: 0.25rem; overflow-y: scroll; flex-direction: column; border: 1px solid green; @@ -46,16 +49,13 @@ div { box-sizing: border-box; flex-direction: column; position: relative; + border: 2px solid #444; } .Face:hover { cursor: pointer; } -.Face .Image { - border: 0.25rem solid transparent; -} - .ClusterEditor { display: flex; flex-direction: column; @@ -82,12 +82,17 @@ div { background-position: 50% 50% !important; } -.Face:hover .Image { - border: 0.25rem solid yellow; +.Active { + filter: brightness(1.25); + border-color: orange !important; } -.Face.Selected .Image { - border: 0.25rem solid blue; +.Face:hover { + border-color: yellow; +} + +.Face.Selected { + border-color: blue; } .Face .Title { @@ -129,5 +134,6 @@ div { .Cluster .Faces { display: grid; + gap: 0.25rem; grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); } \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 6e1314d..94d5f0c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,7 +12,7 @@ import './App.css'; const base = process.env.PUBLIC_URL; /* /identities -- set in .env */ -const makeFaceBoxes = (photo: any, dimensions: any): any => { +const makeFaceBoxes = (photo: any, dimensions: any, onFaceClick: any): any => { const faces: FaceData[] = photo.faces; let width: number, height: number, offsetLeft = 0, offsetTop = 0; @@ -32,20 +32,29 @@ const makeFaceBoxes = (photo: any, dimensions: any): any => { offsetTop = 0; } - return faces.map((face: FaceData) => ( -
- )); + return faces.map((face: FaceData) => { + const faceId = face.faceId; + const identityId = face.identityId; + return ( +
{ onFaceClick(e, face) }} + onMouseEnter={(e) => { onFaceMouseEnter(e, face) }} + onMouseLeave={(e) => { onFaceMouseLeave(e, face) }} + /> + ) + }); }; -const Photo = ({ photoId }: any) => { +const Photo = ({ photoId, onFaceClick }: any) => { const [image, setImage] = useState(undefined); const ref = useRef(null); const [dimensions, setDimensions] = React.useState({width: 0, height: 0}); @@ -54,8 +63,8 @@ const Photo = ({ photoId }: any) => { if (image === undefined || dimensions.height === 0) { return <>; } - return makeFaceBoxes(image, dimensions); - }, [image, dimensions]); + return makeFaceBoxes(image, dimensions, onFaceClick); + }, [image, dimensions, onFaceClick]); useEffect(() => { if (!ref.current) { @@ -92,19 +101,56 @@ const Photo = ({ photoId }: any) => { return (
{ faces }
); }; -const Face = ({ faceId, onClick, title, ...rest }: any) => { +const onFaceMouseEnter = (e: any, face: FaceData) => { + const faceId = face.faceId; + const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)]; + + if (face.identity) { + const identityId = face.identity.identityId; + els.splice(0, 0, + ...document.querySelectorAll(`[data-identity-id="${identityId}"]`)); + } + + els.forEach(el => { + el.classList.add('Active'); + }) +}; + +const onFaceMouseLeave = (e: any, face: FaceData) => { + const faceId = face.faceId; + const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)]; + + if (face.identity) { + const identityId = face.identity.identityId; + els.splice(0, 0, + ...document.querySelectorAll(`[data-identity-id="${identityId}"]`)); + } + + els.forEach(el => { + el.classList.remove('Active'); + }) +}; + +const Face = ({ face, onFaceClick, title, ...rest }: any) => { + const faceId = face.faceId; const idPath = String(faceId % 100).padStart(2, '0'); return ( -
{ onClick(e, faceId) }} +
{ onFaceClick(e, face) }} + onMouseEnter={(e) => { onFaceMouseEnter(e, face) }} + onMouseLeave={(e) => { onFaceMouseLeave(e, face) }} className='Face'>
{title}
@@ -113,47 +159,54 @@ const Face = ({ faceId, onClick, title, ...rest }: any) => { }; type ClusterProps = { - identity: Identity, + identity: IdentityData, setImage(image: number): void, setSelected(selected: number[]): void, - setIdentity(identity: Identity): void + setIdentity(identity: IdentityData): void }; const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps) => { const relatedFacesJSX = useMemo(() => { - const faceClicked = async (e: any, id: any) => { + const faceClicked = async (e: any, face: FaceData) => { if (!identity) { return; } const el = e.currentTarget; - const face = identity.relatedFaces.find(item => item.faceId === id); - if (!face) { + + /* Control -- select / deselect single item */ + if (e.ctrlKey) { + el.classList.toggle('Selected'); + const selected = [...el.parentElement + .querySelectorAll('.Selected')] + .map((face: any) => face.getAttribute('data-face-id')); + setSelected(selected); return; - } + } + + /* Shift -- select groups */ if (e.shiftKey) { - e.stopPropagation(); - e.preventDefault(); - setImage(face.photoId); return; } - el.classList.toggle('Selected'); - const selected = [...el.parentElement - .querySelectorAll('.Selected')] - .map((face: any) => face.getAttribute('data-face-id')); - setSelected(selected); - console.log(face); + + /* Default to load image */ + e.stopPropagation(); + e.preventDefault(); + setImage(face.photoId); } if (identity === undefined) { return <>; } - return identity.relatedFaces.map(face => - + style={{ + display: "flex", + alignItems: "center"}}> + +
); }, [identity, setImage, setSelected]); @@ -233,6 +286,7 @@ type FaceData = { firstName: string, middleName: string, displayName: string, + identity: IdentityData, identityId: number, distance: number, descriptors: any[], @@ -242,40 +296,33 @@ type FaceData = { left: number, }; -type Identity = { +type IdentityData = { lastName: string, middleName: string, firstName: string, descriptors: number[], - id: number + identityId: number displayName: string, relatedFaces: FaceData[] }; interface IdentitiesProps { - setIdentity(identity: Identity): void, - identities: Identity[] + identities: IdentityData[], + onFaceClick(e: any, face: FaceData): void }; -const Identities = ({ identities, setIdentity } : IdentitiesProps) => { +const Identities = ({ identities, onFaceClick } : IdentitiesProps) => { const identitiesJSX = useMemo(() => { - const loadIdentity = async (id: number) => { - const res = await window.fetch(`${base}/api/v1/identities/${id}`); - const data = await res.json(); - setIdentity(data[0]); - }; - return identities.map((identity) => { const face = identity.relatedFaces[0]; return ( loadIdentity(identity.id)} + face={face} + onFaceClick={onFaceClick} title={identity.displayName}/> ); }); - }, [ setIdentity, identities ]); + }, [ identities, onFaceClick ]); return (
@@ -293,27 +340,50 @@ const Button = ({ onClick, children }: any) => { }; const App = () => { - const [identities, setIdentities] = useState([]); + const [identities, setIdentities] = useState([]); + const { identityId, faceId } = useParams(); const [identity, setIdentity] = useState(undefined); const [image, setImage] = useState(0); const { loading, data } = useApi( `${base}/api/v1/identities` ); const [selected, setSelected] = useState([]); - const { identityId, faceId } = useParams(); - console.log({ identityId, faceId}); + const loadIdentity = async (identityId: number) => { + try { + const res = await window.fetch(`${base}/api/v1/identities/${identityId}`); + const data = await res.json(); + setIdentity(data[0]); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + if (identityId !== undefined && !isNaN(+identityId)) { + loadIdentity(+identityId); + } + + if (faceId !== undefined && !isNaN(+faceId)) { + setImage(+faceId); + } + }, []); useEffect(() => { if (data && data.length) { - setIdentities(data as Identity[]); + data.forEach((identity: IdentityData) => { + identity.relatedFaces.forEach(face => { + face.identity = identity; + }); + }); + setIdentities(data as IdentityData[]); } }, [data]); const removeSelected = async () => { try { const res = await window.fetch( - `${base}/api/v1/identities/faces/remove/${identity.id}`, { + `${base}/api/v1/identities/faces/remove/${identity.identityId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ faces: selected }) @@ -333,6 +403,30 @@ const App = () => { } }; + const onFaceClick = (e: any, face: FaceData) => { + if (!face.identity) { + console.log(`Face ${face.faceId} does not have an Identity`); + return; + } + const identityId = face.identity.identityId; + console.log(face.identity); + const identitiesEl = document.querySelector('.Identities'); + if (!identitiesEl) { + return; + } + const faceIdentity = identitiesEl.querySelector( + `[data-identity-id="${identityId}"]`); + if (!faceIdentity) { + return; + } + faceIdentity.scrollIntoView() + }; + + const identitiesOnFaceClick = (e: any, face: FaceData) => { + const identityId = face.identity.identityId; + loadIdentity(identityId); + } + return (
@@ -357,10 +451,11 @@ const App = () => { {image === 0 &&
Select image to view
} - {image !== 0 && } + {image !== 0 && }
- { !loading && } + { !loading && }
); diff --git a/client/src/useApi.tsx b/client/src/useApi.tsx index 7416bf9..e336e4d 100644 --- a/client/src/useApi.tsx +++ b/client/src/useApi.tsx @@ -16,6 +16,7 @@ const useApi = (_url: string, _options?: {}) : UseApi => { if (_url === '') { return; } + const fetchApi = async () => { console.log(`Fetching ${_url}...`); try { diff --git a/server/development.location b/server/development.location index 99e1a0c..189aec2 100644 --- a/server/development.location +++ b/server/development.location @@ -1,4 +1,8 @@ # DEVELOPMENT -- use npm development server on port 3000 (entrypoint.sh) +location /identities/api/v1/ { + rewrite ^/identities/api/v1/(.*)$ https://${host}/api/v1/$1 permanent; +} + location /identities { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -12,11 +16,3 @@ location /identities { proxy_set_header Connection "Upgrade"; proxy_pass https://localhost:3000; } - -location /wsapp/ { - proxy_pass http://wsbackend; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - proxy_set_header Host $host; -} \ No newline at end of file diff --git a/server/production.location b/server/production.location index a19a9b5..0d57dd9 100644 --- a/server/production.location +++ b/server/production.location @@ -1,4 +1,8 @@ # PRODUCTION -- pre-built source +location /identities/api/v1/ { + rewrite ^/identities/api/v1/(.*)$ https://${host}/api/v1/$1 permanent; +} + location /identities { try_files $uri $uri/ =404; alias /website/client/build; diff --git a/server/routes/identities.js b/server/routes/identities.js index 156377e..4dd6708 100755 --- a/server/routes/identities.js +++ b/server/routes/identities.js @@ -174,6 +174,7 @@ router.get("/:id?", async (req, res) => { identity[key] = ''; } }); + identity.identityId = identity.id; const relatedFaces = identity.relatedFaceIds.split(","), relatedFacePhotos = identity.relatedFacePhotoIds.split(","); @@ -197,6 +198,7 @@ router.get("/:id?", async (req, res) => { ); return { + identityId: identity.id, faceId, photoId: relatedFacePhotos[index], distance @@ -215,6 +217,7 @@ router.get("/:id?", async (req, res) => { identity.relatedFaces = [ identity.relatedFaces[0] ]; } + delete identity.id; delete identity.descriptors; delete identity.relatedFaceIds; delete identity.relatedFacePhotoIds; diff --git a/server/routes/photos.js b/server/routes/photos.js index 1655e7b..1c3f110 100755 --- a/server/routes/photos.js +++ b/server/routes/photos.js @@ -1127,8 +1127,8 @@ router.get("/:id", async (req, res) => { if (face.identityId) { const results = await photoDB.sequelize.query( ` - SELECT displayName,firstName,lastName,middleName FROM identities - WHERE id=:id + SELECT id AS identityId,displayName,firstName,lastName,middleName FROM identities + WHERE identityId=:id `, { replacements: { id: face.identityId }, type: photoDB.Sequelize.QueryTypes.SELECT @@ -1136,7 +1136,6 @@ router.get("/:id", async (req, res) => { ); face.identity = results[0]; } - delete face.identityId; }); return res.status(200).json(photo);