From ac733af3b535a09b543eb821f587b38dfc14b05c Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Thu, 11 Jun 2026 09:53:30 +0100 Subject: [PATCH] Add an initial organisation flow --- package.json | 2 + src/App.tsx | 69 ++++---- src/Bridges.tsx | 163 ++++++++++++++++++ src/CreateOrg.tsx | 63 +++---- src/OrgContext.tsx | 25 --- src/OrgPanel.tsx | 76 ++++++++ src/Organisations.tsx | 51 ++++++ src/home.tsx | 11 +- src/hooks/OrgContext.ts | 36 ++++ src/hooks/RefreshContext.ts | 19 ++ src/{UserContext.tsx => hooks/UserContext.ts} | 7 +- src/profile.tsx | 9 +- 12 files changed, 432 insertions(+), 99 deletions(-) create mode 100644 src/Bridges.tsx delete mode 100644 src/OrgContext.tsx create mode 100644 src/OrgPanel.tsx create mode 100644 src/Organisations.tsx create mode 100644 src/hooks/OrgContext.ts create mode 100644 src/hooks/RefreshContext.ts rename src/{UserContext.tsx => hooks/UserContext.ts} (89%) diff --git a/package.json b/package.json index 8f1e67c..3dbd1a7 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,14 @@ "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/react-highlight-words": "^0.20.1", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "react-highlight-words": "^0.21.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12" diff --git a/src/App.tsx b/src/App.tsx index 93e6b1b..55cd22b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,19 +5,20 @@ import React, {useState} from 'react'; import { SettingOutlined, UserOutlined, + DeploymentUnitOutlined, } from '@ant-design/icons'; import {Breadcrumb, type MenuProps} from 'antd'; import { Layout, Menu, theme } from 'antd'; import Home from "./Home.tsx"; import {Route, Routes, useLocation, useNavigate} from "react-router"; import Profile from "./Profile.tsx"; -import {type UserContextType} from './UserContext.tsx'; -import {UserContext} from './UserContext.tsx'; -import type {userObject} from "./UserContext.tsx"; +import {type UserContextType} from './hooks/UserContext.ts'; +import {UserContext} from './hooks/UserContext.ts'; +import type {userObject} from "./hooks/UserContext.ts"; -import {type OrgContextType} from './OrgContext.tsx'; -import {OrgContext} from './OrgContext.tsx'; -import type {orgObject} from "./OrgContext.tsx"; +import Organisations from "./Organisations.tsx"; +import {RefreshContext} from "./hooks/RefreshContext.ts"; +import Bridges from "./Bridges.tsx"; const { Header, Content, Sider } = Layout; @@ -66,28 +67,16 @@ const App: React.FC = () => { first_name: "", last_name: "", email: "", - organisations: [] + organisations: [], + id: -1 }, setCurrentUser: () => {} } - - const defaultOrg: OrgContextType = { - currentOrg: { - billing_contact: "", - owner_contact: "", - name: "", - status: "", - root_user: "", - security_contact: "", - }, - setCurrentOrg: () => {} - } - const [currentUser, setCurrentUser] = useState(defaultUser.currentUser); - const [currentOrg, setCurrentOrg] = useState(defaultOrg.currentOrg); + const [refreshKey, setRefreshKey] = useState(1); + + - // STRILL NEEDS SOMETHING TO get CURRENT USER ORGS and map first one to a CURRENT ORG fetched from the API - // STILL NEEDS THE WRAPPER IN THE RETURN STATEMETNS /***************************** GETTING CURRENT USER FROM API *****************************/ @@ -108,7 +97,7 @@ const App: React.FC = () => { console.error(e); } })(); - }, [auth]); + }, [auth, refreshKey]); if (!currentUser) { @@ -142,8 +131,14 @@ const App: React.FC = () => { case 'profile': navigate('/profile'); break; + case 'orgs': + navigate('/organisations'); + break; case 'logout': - auth.removeUser().then(r => navigate('/profile')); + auth.removeUser().then(() => navigate('/profile')); + break; + case 'bridges': + navigate('/bridges'); break; default: console.log('Clicked item:', e.key); @@ -151,19 +146,26 @@ const App: React.FC = () => { }; - //const organisations: [] = current_user.organisations; - - const side_nav_items: MenuItem[] = currentUser.organisations && [ - getItem('Files', '9', ), - ]; + let side_nav_items: MenuItem[] = [] const top_nav_items = [ getItem(currentUser.first_name + " " +currentUser.last_name , 'account', , [getItem('Account details', 'profile'), getItem('Log out', 'logout')]), - getItem("Organisation Settings", 'orgsettings', ), ]; + + if (currentUser.organisations.length > 0) { + + side_nav_items = [ getItem('My organisations', 'orgs', ), + getItem('Bridges', 'bridges', ), ]; + + const currentOrg = currentUser.organisations[0]['name'] + top_nav_items.push(getItem(currentOrg +" Settings", 'orgsettings', )); + + } + return ( - + +

SR22

@@ -206,12 +208,15 @@ const App: React.FC = () => { }/> {currentUser && }/> } + {currentUser && }/> } + {currentUser && }/> } + ); diff --git a/src/Bridges.tsx b/src/Bridges.tsx new file mode 100644 index 0000000..1705451 --- /dev/null +++ b/src/Bridges.tsx @@ -0,0 +1,163 @@ +import { useRef, useState } from 'react'; +import { SearchOutlined } from '@ant-design/icons'; +import type { InputRef, TableColumnsType, TableColumnType } from 'antd'; +import { Button, Input, Space, Table } from 'antd'; +import type { FilterDropdownProps } from 'antd/es/table/interface'; +import Highlighter from 'react-highlight-words'; +import { QRCode, theme } from 'antd'; +const { useToken } = theme; +interface BridgeDataType { + fingerprint: string; + bridgeline: string; + qrCodeText: string; +} + +const Bridges = () => { + + const bridgeLines: BridgeDataType[] = [ + {fingerprint: '0C6DDE3F9FC377A5E69DC044E81C857277148D71', bridgeline: "obfs4 62.224.107.157:443 0C6DDE3F9FC377A5E69DC044E81C857277148D71 cert=6vp+cyZmJOp+QFtytv9Ca+Z+ASF4l7r12MeEevvufl4OcDimBhjQlxctjfgdCmnT7iu7Lg iat-mode=0", qrCodeText: "" }, + {fingerprint:'8DFC222D2295A909B34B3AAAA584648EE6FAF14D', bridgeline: "obfs4 144.31.125.189:2063 8DFC222D2295A909B34B3AAAA584648EE6FAF14D cert=3BPE8q1dHF1AWoqsQEDGZHrRXPw/AaTwyM7YLGMOfFlxY6qRAlW1Jq0LlzKbe4Gh9+SacA iat-mode=0", qrCodeText: "" }, + {fingerprint: '0C6DDE3F9FC377A5E69DC044E81C857277148D71', bridgeline: "obfs4 62.224.107.157:443 0C6DDE3F9FC377A5E69DC044E81C857277148D71 cert=6vp+cyZmJOp+QFtytv9Ca+Z+ASF4l7r12MeEevvufl4OcDimBhjQlxctjfgdCmnT7iu7Lg iat-mode=0", qrCodeText: ""}, + {fingerprint:'8DFC222D2295A909B34B3AAAA584648EE6FAF14D', bridgeline: "obfs4 144.31.125.189:2063 8DFC222D2295A909B34B3AAAA584648EE6FAF14D cert=3BPE8q1dHF1AWoqsQEDGZHrRXPw/AaTwyM7YLGMOfFlxY6qRAlW1Jq0LlzKbe4Gh9+SacA iat-mode=0", qrCodeText: ""}, + ]; + + + function makeQRCodeText(bridgeline: string) { + return '["' + bridgeline + '"]'; + } + + bridgeLines.forEach((line) => {line.qrCodeText = makeQRCodeText(line.bridgeline)}); + console.log(bridgeLines); + const { token } = useToken(); + + const [searchText, setSearchText] = useState(''); + const [searchedColumn, setSearchedColumn] = useState(''); + const searchInput = useRef(null); + + const handleSearch = ( + selectedKeys: string[], + confirm: FilterDropdownProps['confirm'], + dataIndex: DataIndex, + ) => { + confirm(); + setSearchText(selectedKeys[0]); + setSearchedColumn(dataIndex); + }; + + const handleReset = (clearFilters: () => void) => { + clearFilters(); + setSearchText(''); + }; + + type DataIndex = keyof BridgeDataType; + const getColumnSearchProps = (dataIndex: DataIndex): TableColumnType => ({ + filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( +
e.stopPropagation()}> + setSelectedKeys(e.target.value ? [e.target.value] : [])} + onPressEnter={() => handleSearch(selectedKeys as string[], confirm, dataIndex)} + style={{ marginBottom: 8, display: 'block' }} + /> + + + + + + +
+ ), + filterIcon: (filtered: boolean) => ( + + ), + onFilter: (value, record) => + record[dataIndex] + .toString() + .toLowerCase() + .includes((value as string).toLowerCase()), + filterDropdownProps: { + onOpenChange(open) { + if (open) { + setTimeout(() => searchInput.current?.select(), 100); + } + }, + }, + render: (text) => + searchedColumn === dataIndex ? ( + + ) : ( + text + ), + }); + + const bridgesColumns: TableColumnsType = [ + {title: 'Fingerprint', + dataIndex: 'fingerprint', + key: 'fingerprint', + width: '30%', + ...getColumnSearchProps('fingerprint')}, + + {title: 'Bridgeline', + dataIndex: 'bridgeline', + key: 'bridgeline', + width: '40%', + ...getColumnSearchProps('bridgeline')}, + + {title: 'QR Code', + width: '40%', + key: 'qrCodeText', + dataIndex: 'qrCodeText', + render: (text, record) => {return ()} + }, + + ] + + + return( + <> + + columns={bridgesColumns} dataSource={bridgeLines} />; + ); + +} + +export default Bridges; \ No newline at end of file diff --git a/src/CreateOrg.tsx b/src/CreateOrg.tsx index 6b11e6d..4895ac2 100644 --- a/src/CreateOrg.tsx +++ b/src/CreateOrg.tsx @@ -1,25 +1,24 @@ // src/Posts.jsx -import {useUserContext} from './UserContext'; +import {useUserContext} from './hooks/UserContext.ts'; import { Button, Form, Input } from 'antd'; //import React, {useEffect, useState} from "react"; import {useAuth} from "react-oidc-context"; +import {RefreshContext} from "./hooks/RefreshContext.ts"; +import {useContext} from "react"; const CreateOrg = () => { const { currentUser } = useUserContext(); const [form] = Form.useForm(); const auth = useAuth(); - - + const { refreshKey, setRefreshKey } = useContext(RefreshContext); + console.log(refreshKey); async function submitOrgData(values: FormData) { if (!auth.user) { return } try { - console.log("in here"); const [name, q1, q2, q3] = Object.values(values); - console.log("end here"); - const data = { "name": name, "intake_questionnaire": { @@ -28,16 +27,18 @@ const CreateOrg = () => { "question_three": q3, } } - console.log(data) const token = auth.user.access_token; const requestOptions = { method: 'POST', body: JSON.stringify(data), headers: {Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json'} + 'Content-Type': 'application/json'} } - const response = await fetch("/api/v1/org/", requestOptions); + const response = await fetch("/api/v1/org", requestOptions); await response.json(); + setRefreshKey(prev => prev + 1); + console.log(refreshKey); + } catch (e) { console.error(e); } @@ -55,29 +56,29 @@ const CreateOrg = () => { return ( <> -
- - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + -
- + + ); }; diff --git a/src/OrgContext.tsx b/src/OrgContext.tsx deleted file mode 100644 index 582cedc..0000000 --- a/src/OrgContext.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { createContext, useContext } from 'react'; - -interface orgObject { - name: string; - status: "partial", - root_user: "string", - owner_contact: "string", - billing_contact: "string", - security_contact: "string" -} - -export interface OrgContextType { - currentOrg: orgObject; - setCurrentOrg: React.Dispatch>; -} - -export const OrgContext = createContext(undefined); - -export const useOrgContext = () => { - const context: OrgContextType | undefined = useContext(OrgContext); - if (context === undefined) { - throw new Error('Organisation context not found'); - } - return context; -}; \ No newline at end of file diff --git a/src/OrgPanel.tsx b/src/OrgPanel.tsx new file mode 100644 index 0000000..2bf3627 --- /dev/null +++ b/src/OrgPanel.tsx @@ -0,0 +1,76 @@ +import {Button, Card, Space} from 'antd'; +import type {OrgObject} from "./hooks/OrgContext.ts"; +import {useUserContext} from "./hooks/UserContext.ts"; +import {useAuth} from "react-oidc-context"; +import {useContext} from "react"; +import {RefreshContext} from "./hooks/RefreshContext.ts"; + +const OrgPanel = (props: OrgObject) => { + + const auth = useAuth(); + const { refreshKey, setRefreshKey } = useContext(RefreshContext); + const {currentUser} = useUserContext(); + async function removeUserFromOrg(org_id: number) { + if (!auth.user) { + return + } + try { + const token = auth.user.access_token; + const requestOptions = { + method: 'DELETE', + headers: {Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json'} + } + const response = await fetch("/api/v1/org/user?org_id=" + org_id + "&user_id=" + currentUser.id, requestOptions); + await response.json(); + setRefreshKey(prev => prev + 1); + console.log(refreshKey) + + } catch (e) { + console.error(e); + } + } + + + async function removeOrg(org_id: number) { + if (!auth.user) { + return + } + try { + const token = auth.user.access_token; + const requestOptions = { + method: 'DELETE', + headers: {Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json'} + } + await fetch("/api/v1/org/self?org_id=" + org_id, requestOptions); + setRefreshKey(prev => prev + 1); + console.log(refreshKey) + + } catch (e) { + console.error(e); + } + } + + return ( + + More} style={{width: 300}}> +

Validated? {props.status}

+

Root User: {props.root_user_email}

+

Billing Contact: {props.billing_contact?.email || 'Not set'}

+

Questionnaire

+

Q1: {props.intake_questionnaire?.question_one || 'Not set'}

+

Q2: {props.intake_questionnaire?.question_two || 'Not set'}

+

Q3: {props.intake_questionnaire?.question_three || 'Not set'}

+ + + +
+
+ ); +} +export default OrgPanel; \ No newline at end of file diff --git a/src/Organisations.tsx b/src/Organisations.tsx new file mode 100644 index 0000000..e6ef54e --- /dev/null +++ b/src/Organisations.tsx @@ -0,0 +1,51 @@ +import React, {useState} from "react"; +import OrgPanel from "./OrgPanel"; +import {useAuth} from "react-oidc-context"; +import type {OrgObject} from "./hooks/OrgContext.ts"; +import type { JSX } from "react/jsx-runtime"; +import CreateOrg from "./CreateOrg.tsx"; +import {useRefreshContext} from "./hooks/RefreshContext.ts"; + +const Organisations: React.FC = () => { + const auth = useAuth(); + const [orgData, setCurrentOrgData] = useState(); + const { refreshKey } = useRefreshContext(); + React.useEffect(() => { + const fetchOrgData = async () => { + console.log("Fetching org data"); + if (!auth.user) return + try { + const requestOptions = { + method: 'GET', + headers: { + Authorization: `Bearer ${auth.user.access_token}`, + 'Content-Type': 'application/json' + } + } + const response = await fetch("/api/v1/user/self/orgs", requestOptions); + const orgsObject = await response.json(); + setCurrentOrgData(orgsObject['organisations']) + + } catch (e) { + console.error(e); + } + } + fetchOrgData() + .catch(console.error); + }, [auth.user, refreshKey]); + + console.log(orgData); + + let panels: JSX.Element[] = []; + if (orgData) { + panels = orgData.map(org => ); + } + return(<> +

Your organisations

+ {panels} +

Add a new organisation

+ + ); +} + +export default Organisations; \ No newline at end of file diff --git a/src/home.tsx b/src/home.tsx index ae246c7..c767b30 100644 --- a/src/home.tsx +++ b/src/home.tsx @@ -1,15 +1,14 @@ import {Breadcrumb, theme} from "antd"; -import React, {useContext} from "react"; +import React from "react"; //import {useOrgContext} from './OrgContext'; import CreateOrg from "./CreateOrg.tsx"; -import {useUserContext} from "./UserContext.tsx"; +import {useUserContext} from "./hooks/UserContext.ts"; const Home: React.FC = () => { const { token: { colorBgContainer, borderRadiusLG }, } = theme.useToken(); - //const { currentOrg } = useOrgContext(); const { currentUser } = useUserContext(); console.log(currentUser.organisations); if (currentUser.organisations.length === 0) { @@ -19,8 +18,9 @@ const Home: React.FC = () => { } + //TODO: fix this breadcrumb return (<> - +
{ borderRadius: borderRadiusLG, }} > - Bill is a cat. +

Welcome to your dashboard, {currentUser.first_name}!

+
) } diff --git a/src/hooks/OrgContext.ts b/src/hooks/OrgContext.ts new file mode 100644 index 0000000..0db3f16 --- /dev/null +++ b/src/hooks/OrgContext.ts @@ -0,0 +1,36 @@ +import React, {createContext, useContext} from 'react'; + +export interface Contact { + email: string; + id: number; +} + +export interface OrgObject { + name: string, + status: "partial", + root_user_email: string, + billing_contact: Contact, + owner_contact: Conotact, + security_contact:Cntact, + organisation_id: number, + intake_questionnaire: { + question_one: string, + question_two: string, + question_three: string + }, +} + +export interface OrgContextType { + currentOrg: OrgObject; + setCurrentOrg: React.Dispatch>; +} + +export const OrgContext = createContext(undefined); + +export const useOrgContext = () => { + const context: OrgContextType | undefined = useContext(OrgContext); + if (context === undefined) { + throw new Error('Organisation context not found'); + } + return context; +}; \ No newline at end of file diff --git a/src/hooks/RefreshContext.ts b/src/hooks/RefreshContext.ts new file mode 100644 index 0000000..3b3572a --- /dev/null +++ b/src/hooks/RefreshContext.ts @@ -0,0 +1,19 @@ +import React, {createContext, useContext} from 'react'; + +export interface RefreshType { + refreshKey: number; + setRefreshKey: React.Dispatch>; +} + +export const RefreshContext = createContext({ + refreshKey: 1, + setRefreshKey: () => {}, +}); + +export const useRefreshContext = () => { + const context: RefreshType | undefined = useContext(RefreshContext); + if (context === undefined) { + throw new Error('Organisation refresh context not found'); + } + return context; +}; \ No newline at end of file diff --git a/src/UserContext.tsx b/src/hooks/UserContext.ts similarity index 89% rename from src/UserContext.tsx rename to src/hooks/UserContext.ts index 9025108..dcc94ee 100644 --- a/src/UserContext.tsx +++ b/src/hooks/UserContext.ts @@ -5,7 +5,8 @@ export interface userObject { first_name: string; last_name: string; email: string; - organisations: string[]; + organisations: { name: string, id: number }[]; + id: number; } export interface UserContextType { @@ -21,4 +22,6 @@ export const useUserContext = () => { throw new Error('User context not found!'); } return context; -}; \ No newline at end of file +}; + + diff --git a/src/profile.tsx b/src/profile.tsx index 5618d45..9285780 100644 --- a/src/profile.tsx +++ b/src/profile.tsx @@ -1,6 +1,6 @@ // src/Posts.jsx -import {useUserContext} from './UserContext'; +import { useUserContext} from './hooks/UserContext.ts'; const Profile = () => { const { currentUser } = useUserContext(); @@ -8,14 +8,15 @@ const Profile = () => { if (!currentUser) { return
Loading...
; } - + console.log(currentUser); + console.log(currentUser.organisations); + const userOrgs: { name: string, id: number }[] = currentUser.organisations; return ( <>

User: {currentUser.first_name} {currentUser.last_name}

Email: {currentUser.email}

-

Orgs: {currentUser.organisations}

+

Current Orgs: {userOrgs[0].name}

- ); };