feat: prettier format

This commit is contained in:
Iain Learmonth 2026-06-22 15:50:35 +01:00
parent 6eed4dc75d
commit 734dc440c6
15 changed files with 955 additions and 791 deletions

View file

@ -17,9 +17,9 @@ If you are developing a production application, we recommend updating the config
```js ```js
export default defineConfig([ export default defineConfig([
globalIgnores(['dist']), globalIgnores(["dist"]),
{ {
files: ['**/*.{ts,tsx}'], files: ["**/*.{ts,tsx}"],
extends: [ extends: [
// Other configs... // Other configs...
@ -34,40 +34,40 @@ export default defineConfig([
], ],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'], project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
// other options... // other options...
}, },
}, },
]) ]);
``` ```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js ```js
// eslint.config.js // eslint.config.js
import reactX from 'eslint-plugin-react-x' import reactX from "eslint-plugin-react-x";
import reactDom from 'eslint-plugin-react-dom' import reactDom from "eslint-plugin-react-dom";
export default defineConfig([ export default defineConfig([
globalIgnores(['dist']), globalIgnores(["dist"]),
{ {
files: ['**/*.{ts,tsx}'], files: ["**/*.{ts,tsx}"],
extends: [ extends: [
// Other configs... // Other configs...
// Enable lint rules for React // Enable lint rules for React
reactX.configs['recommended-typescript'], reactX.configs["recommended-typescript"],
// Enable lint rules for React DOM // Enable lint rules for React DOM
reactDom.configs.recommended, reactDom.configs.recommended,
], ],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'], project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
// other options... // other options...
}, },
}, },
]) ]);
``` ```

View file

@ -1,15 +1,15 @@
import js from '@eslint/js' import js from "@eslint/js";
import globals from 'globals' import globals from "globals";
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from 'eslint-plugin-react-refresh' import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from 'typescript-eslint' import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from 'eslint/config' import { defineConfig, globalIgnores } from "eslint/config";
import eslintConfigPrettier from "eslint-config-prettier"; import eslintConfigPrettier from "eslint-config-prettier";
export default defineConfig([ export default defineConfig([
globalIgnores(['dist']), globalIgnores(["dist"]),
{ {
files: ['**/*.{ts,tsx}'], files: ["**/*.{ts,tsx}"],
extends: [ extends: [
js.configs.recommended, js.configs.recommended,
tseslint.configs.recommended, tseslint.configs.recommended,
@ -21,4 +21,4 @@ export default defineConfig([
globals: globals.browser, globals: globals.browser,
}, },
}, },
]) ]);

View file

@ -1,8 +1,6 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:recommended"],
"config:recommended"
],
"minimumReleaseAge": "14 days", "minimumReleaseAge": "14 days",
"gitAuthor": "Renovate<noreply@sr2.uk>" "gitAuthor": "Renovate<noreply@sr2.uk>"
} }

View file

@ -1,252 +1,273 @@
import "./App.css";
import './App.css'
import { useAuth, hasAuthParams } from "react-oidc-context"; import { useAuth, hasAuthParams } from "react-oidc-context";
import React, {useState} from 'react'; import React, { useState } from "react";
import { import {
SettingOutlined, SettingOutlined,
UserOutlined, UserOutlined,
DeploymentUnitOutlined, DeploymentUnitOutlined,
} from '@ant-design/icons'; } from "@ant-design/icons";
import {Breadcrumb, type MenuProps} from 'antd'; import { Breadcrumb, type MenuProps } from "antd";
import { Layout, Menu, theme } from 'antd'; import { Layout, Menu, theme } from "antd";
import Home from "./Home.tsx"; import Home from "./Home.tsx";
import {Route, Routes, useLocation, useNavigate} from "react-router"; import { Route, Routes, useLocation, useNavigate } from "react-router";
import Profile from "./Profile.tsx"; import Profile from "./Profile.tsx";
import {type UserContextType} from './hooks/UserContext.ts'; import { type UserContextType } from "./hooks/UserContext.ts";
import {UserContext} from './hooks/UserContext.ts'; import { UserContext } from "./hooks/UserContext.ts";
import type {userObject} from "./hooks/UserContext.ts"; import type { userObject } from "./hooks/UserContext.ts";
import Organisations from "./Organisations.tsx"; import Organisations from "./Organisations.tsx";
import {RefreshContext} from "./hooks/RefreshContext.ts"; import { RefreshContext } from "./hooks/RefreshContext.ts";
import Bridges from "./Bridges.tsx"; import Bridges from "./Bridges.tsx";
import {OrgContext, type OrgContextType, type OrgObject} from "./hooks/OrgContext.ts"; import {
OrgContext,
type OrgContextType,
type OrgObject,
} from "./hooks/OrgContext.ts";
import CreateOrgFlow from "./CreateOrgFlow.tsx"; import CreateOrgFlow from "./CreateOrgFlow.tsx";
const { Header, Content, Sider } = Layout; const { Header, Content, Sider } = Layout;
type MenuItem = Required<MenuProps>['items'][number]; type MenuItem = Required<MenuProps>["items"][number];
function getItem( function getItem(
label: React.ReactNode, label: React.ReactNode,
key: React.Key, key: React.Key,
icon?: React.ReactNode, icon?: React.ReactNode,
children?: MenuItem[], children?: MenuItem[],
): MenuItem { ): MenuItem {
return { return {
key, key,
icon, icon,
children, children,
label, label,
} as MenuItem; } as MenuItem;
} }
const App: React.FC = () => { const App: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { const {
token: {colorBgContainer, borderRadiusLG}, token: { colorBgContainer, borderRadiusLG },
} = theme.useToken(); } = theme.useToken();
/******************* /*******************
SIGNING IN WITH OIDC SIGNING IN WITH OIDC
*******************/ *******************/
const auth = useAuth(); const auth = useAuth();
const hasTriedSignin = React.useRef(false); const hasTriedSignin = React.useRef(false);
// automatically sign-in // automatically sign-in
React.useEffect(() => { React.useEffect(() => {
if (!hasAuthParams() && if (
!auth.isAuthenticated && !auth.activeNavigator && !auth.isLoading && !hasAuthParams() &&
!hasTriedSignin.current !auth.isAuthenticated &&
) { !auth.activeNavigator &&
auth.signinRedirect(); !auth.isLoading &&
hasTriedSignin.current = true; !hasTriedSignin.current
} ) {
}, [auth]); auth.signinRedirect();
hasTriedSignin.current = true;
const defaultUser: UserContextType = {
currentUser: {
first_name: "",
last_name: "",
email: "",
organisations: [],
id: -1
},
setCurrentUser: () => {}
} }
}, [auth]);
const defaultOrg: OrgContextType = { const defaultUser: UserContextType = {
currentOrg: { currentUser: {
name: "", first_name: "",
status: "partial", last_name: "",
root_user_email: "", email: "",
billing_contact: {email: "", id: -1}, organisations: [],
owner_contact: {email: "", id: -1}, id: -1,
security_contact: {email: "", id: -1}, },
organisation_id: -1, setCurrentUser: () => {},
intake_questionnaire: { };
question_one: "",
question_two: "",
question_three: "",
},
},
setCurrentOrg: () => {}
}
const [currentUser, setCurrentUser] = useState<userObject>(defaultUser.currentUser); const defaultOrg: OrgContextType = {
const [refreshKey, setRefreshKey] = useState<number>(1); currentOrg: {
const [currentOrg, setCurrentOrg] = useState<OrgObject>(defaultOrg.currentOrg); name: "",
status: "partial",
root_user_email: "",
billing_contact: { email: "", id: -1 },
owner_contact: { email: "", id: -1 },
security_contact: { email: "", id: -1 },
organisation_id: -1,
intake_questionnaire: {
question_one: "",
question_two: "",
question_three: "",
},
},
setCurrentOrg: () => {},
};
const [currentUser, setCurrentUser] = useState<userObject>(
defaultUser.currentUser,
);
const [refreshKey, setRefreshKey] = useState<number>(1);
const [currentOrg, setCurrentOrg] = useState<OrgObject>(
defaultOrg.currentOrg,
);
/***************************** /*****************************
GETTING CURRENT USER FROM API GETTING CURRENT USER FROM API
*****************************/ *****************************/
// const [current_user, setUser] = useState<User | null>(null); // const [current_user, setUser] = useState<User | null>(null);
React.useEffect(() => { React.useEffect(() => {
(async () => { (async () => {
if (!auth.user) { return } if (!auth.user) {
try { return;
const token = auth.user.access_token; }
const response = await fetch("/api/v1/user/self/db", { try {
headers: { const token = auth.user.access_token;
Authorization: `Bearer ${token}`, const response = await fetch("/api/v1/user/self/db", {
}, headers: {
}); Authorization: `Bearer ${token}`,
setCurrentUser(await response.json()); },
} catch (e) { });
console.error(e); setCurrentUser(await response.json());
} } catch (e) {
})(); console.error(e);
}, [auth, refreshKey]); }
})();
}, [auth, refreshKey]);
if (!currentUser) {
return (
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
<div>Determining your existing user...</div>;
</UserContext.Provider>
);
}
if (!currentUser) { if (auth.isLoading) {
return( return (
<UserContext.Provider value={{ currentUser, setCurrentUser }}> <UserContext.Provider value={{ currentUser, setCurrentUser }}>
<div>Determining your existing user...</div>; <div>Signing you in/out...</div>;
</UserContext.Provider> </UserContext.Provider>
); );
}
if (!auth.isAuthenticated) {
return (
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
<div>Unable to log in</div>;
</UserContext.Provider>
);
}
const onClick: MenuProps["onClick"] = (e) => {
switch (e.key) {
case "profile":
navigate("/profile");
break;
case "orgs":
navigate("/create-org-flow");
break;
case "logout":
auth.removeUser().then(() => navigate("/profile"));
break;
case "bridges":
navigate("/bridges");
break;
default:
console.log("Clicked item:", e.key);
} }
};
if (auth.isLoading) { let side_nav_items: MenuItem[] = [];
return(
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
<div>Signing you in/out...</div>;
</UserContext.Provider>
);
}
if (!auth.isAuthenticated) { const top_nav_items = [
return( getItem(
<UserContext.Provider value={{ currentUser, setCurrentUser }}> currentUser.first_name + " " + currentUser.last_name,
<div>Unable to log in</div>; "account",
</UserContext.Provider> <UserOutlined />,
); [getItem("Account details", "profile"), getItem("Log out", "logout")],
} ),
];
if (currentUser.organisations.length > 0) {
side_nav_items = [
const onClick: MenuProps['onClick'] = (e) => { getItem("Onboarding", "orgs", <SettingOutlined />),
switch (e.key) { getItem("Bridges", "bridges", <DeploymentUnitOutlined />),
case 'profile':
navigate('/profile');
break;
case 'orgs':
navigate('/create-org-flow');
break;
case 'logout':
auth.removeUser().then(() => navigate('/profile'));
break;
case 'bridges':
navigate('/bridges');
break;
default:
console.log('Clicked item:', e.key);
}
};
let side_nav_items: MenuItem[] = []
const top_nav_items = [
getItem(currentUser.first_name + " " +currentUser.last_name , 'account', <UserOutlined />, [getItem('Account details', 'profile'), getItem('Log out', 'logout')]),
]; ];
if (currentUser.organisations.length > 0) { const currentOrg = currentUser.organisations[0]["name"];
top_nav_items.push(
getItem(currentOrg + " Settings", "orgsettings", <SettingOutlined />),
);
}
side_nav_items = [ getItem('Onboarding', 'orgs', <SettingOutlined />), return (
getItem('Bridges', 'bridges', <DeploymentUnitOutlined />), ]; <OrgContext.Provider value={{ currentOrg, setCurrentOrg }}>
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
const currentOrg = currentUser.organisations[0]['name'] <RefreshContext.Provider value={{ refreshKey, setRefreshKey }}>
top_nav_items.push(getItem(currentOrg +" Settings", 'orgsettings', <SettingOutlined />)); <Layout>
<Header style={{ display: "flex", alignItems: "right" }}>
} <div className="demo-logo">
<h1>SR22</h1>
return ( </div>
<OrgContext.Provider value={{ currentOrg, setCurrentOrg }}> <Menu
<UserContext.Provider value={{ currentUser, setCurrentUser }}> theme="dark"
<RefreshContext.Provider value={{ refreshKey, setRefreshKey }}> mode="horizontal"
<Layout> defaultSelectedKeys={["2"]}
<Header style={{ display: 'flex', alignItems: 'right' }}> items={top_nav_items}
<div className="demo-logo" > onClick={onClick}
<h1>SR22</h1> style={{ flex: 1, minWidth: 0, justifyContent: "flex-end" }}
</div> />
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={['2']}
items={top_nav_items}
onClick={onClick}
style={{ flex: 1, minWidth: 0, justifyContent:"flex-end" }}
/>
</Header> </Header>
<Layout> <Layout>
<Sider width={200} style={{ background: colorBgContainer }}> <Sider width={200} style={{ background: colorBgContainer }}>
<Menu <Menu
mode="inline" mode="inline"
defaultSelectedKeys={['1']} defaultSelectedKeys={["1"]}
defaultOpenKeys={['sub1']} defaultOpenKeys={["sub1"]}
style={{ height: '100%', borderInlineEnd: 0 }} style={{ height: "100%", borderInlineEnd: 0 }}
items={side_nav_items} items={side_nav_items}
onClick={onClick} onClick={onClick}
/> />
</Sider> </Sider>
<Layout style={{ padding: '0 24px 24px' }}> <Layout style={{ padding: "0 24px 24px" }}>
<Breadcrumb <Breadcrumb
items={[{ title: 'Home' }, { title: location.pathname.substring(1) }]} items={[
style={{ margin: '16px 0' }} { title: "Home" },
/> { title: location.pathname.substring(1) },
<Content ]}
style={{ style={{ margin: "16px 0" }}
padding: 24, />
margin: 0, <Content
minHeight: 280, style={{
background: colorBgContainer, padding: 24,
borderRadius: borderRadiusLG, margin: 0,
}} minHeight: 280,
> background: colorBgContainer,
borderRadius: borderRadiusLG,
}}
>
<Routes>
<Route path="/" element={<Home />} />
{currentUser && (
<Route path="/profile" element={<Profile />} />
)}
{currentUser && (
<Route
path="/organisations"
element={<Organisations />}
/>
)}
{currentUser && (
<Route path="/create" element={<CreateOrgFlow />} />
)}
<Routes> {currentUser && (
<Route path="/" element={<Home/>}/> <Route path="/bridges" element={<Bridges />} />
{currentUser && <Route path="/profile" element={<Profile/>}/> } )}
{currentUser && <Route path="/organisations" element={<Organisations/>}/> } </Routes>
{currentUser && <Route path="/create" element={<CreateOrgFlow/>}/> } </Content>
</Layout>
{currentUser && <Route path="/bridges" element={<Bridges/>}/> }
</Routes>
</Content>
</Layout>
</Layout> </Layout>
</Layout> </Layout>
</RefreshContext.Provider> </RefreshContext.Provider>
</UserContext.Provider> </UserContext.Provider>
</OrgContext.Provider> </OrgContext.Provider>
);
};
); export default App;
}
export default App

View file

@ -1,161 +1,203 @@
import { useRef, useState } from 'react'; import { useRef, useState } from "react";
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from "@ant-design/icons";
import type { InputRef, TableColumnsType, TableColumnType } from 'antd'; import type { InputRef, TableColumnsType, TableColumnType } from "antd";
import { Button, Input, Space, Table } from 'antd'; import { Button, Input, Space, Table } from "antd";
import type { FilterDropdownProps } from 'antd/es/table/interface'; import type { FilterDropdownProps } from "antd/es/table/interface";
import Highlighter from 'react-highlight-words'; import Highlighter from "react-highlight-words";
import { QRCode } from 'antd'; import { QRCode } from "antd";
interface BridgeDataType { interface BridgeDataType {
fingerprint: string; fingerprint: string;
bridgeline: string; bridgeline: string;
qrCodeText: string; qrCodeText: string;
} }
const Bridges = () => { 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: "",
},
];
const bridgeLines: BridgeDataType[] = [ function makeQRCodeText(bridgeline: string) {
{fingerprint: '0C6DDE3F9FC377A5E69DC044E81C857277148D71', bridgeline: "obfs4 62.224.107.157:443 0C6DDE3F9FC377A5E69DC044E81C857277148D71 cert=6vp+cyZmJOp+QFtytv9Ca+Z+ASF4l7r12MeEevvufl4OcDimBhjQlxctjfgdCmnT7iu7Lg iat-mode=0", qrCodeText: "" }, return '["' + bridgeline + '"]';
{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: ""},
];
bridgeLines.forEach((line) => {
line.qrCodeText = makeQRCodeText(line.bridgeline);
});
console.log(bridgeLines);
function makeQRCodeText(bridgeline: string) { const [searchText, setSearchText] = useState("");
return '["' + bridgeline + '"]'; const [searchedColumn, setSearchedColumn] = useState("");
} const searchInput = useRef<InputRef>(null);
bridgeLines.forEach((line) => {line.qrCodeText = makeQRCodeText(line.bridgeline)}); const handleSearch = (
console.log(bridgeLines); selectedKeys: string[],
confirm: FilterDropdownProps["confirm"],
dataIndex: DataIndex,
) => {
confirm();
setSearchText(selectedKeys[0]);
setSearchedColumn(dataIndex);
};
const [searchText, setSearchText] = useState(''); const handleReset = (clearFilters: () => void) => {
const [searchedColumn, setSearchedColumn] = useState(''); clearFilters();
const searchInput = useRef<InputRef>(null); setSearchText("");
};
const handleSearch = ( type DataIndex = keyof BridgeDataType;
selectedKeys: string[], const getColumnSearchProps = (
confirm: FilterDropdownProps['confirm'], dataIndex: DataIndex,
dataIndex: DataIndex, ): TableColumnType<BridgeDataType> => ({
) => { filterDropdown: ({
confirm(); setSelectedKeys,
setSearchText(selectedKeys[0]); selectedKeys,
setSearchedColumn(dataIndex); confirm,
}; clearFilters,
close,
}) => (
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}>
<Input
//ref={searchInput}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() =>
handleSearch(selectedKeys as string[], confirm, dataIndex)
}
style={{ marginBottom: 8, display: "block" }}
/>
<Space>
<Button
type="primary"
onClick={() =>
handleSearch(selectedKeys as string[], confirm, dataIndex)
}
icon={<SearchOutlined />}
size="small"
style={{ width: 90 }}
>
Search
</Button>
<Button
onClick={() => clearFilters && handleReset(clearFilters)}
size="small"
style={{ width: 90 }}
>
Reset
</Button>
<Button
type="link"
size="small"
onClick={() => {
confirm({ closeDropdown: false });
setSearchText((selectedKeys as string[])[0]);
setSearchedColumn(dataIndex);
}}
>
Filter
</Button>
<Button
type="link"
size="small"
onClick={() => {
close();
}}
>
close
</Button>
</Space>
</div>
),
filterIcon: (filtered: boolean) => (
<SearchOutlined style={{ color: filtered ? "#1677ff" : undefined }} />
),
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 ? (
<Highlighter
highlightStyle={{ backgroundColor: "#ffc069", padding: 0 }}
searchWords={[searchText]}
autoEscape
textToHighlight={text ? text.toString() : ""}
/>
) : (
text
),
});
const handleReset = (clearFilters: () => void) => { const bridgesColumns: TableColumnsType<BridgeDataType> = [
clearFilters(); {
setSearchText(''); title: "Fingerprint",
}; dataIndex: "fingerprint",
key: "fingerprint",
width: "30%",
...getColumnSearchProps("fingerprint"),
},
type DataIndex = keyof BridgeDataType; {
const getColumnSearchProps = (dataIndex: DataIndex): TableColumnType<BridgeDataType> => ({ title: "Bridgeline",
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( dataIndex: "bridgeline",
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> key: "bridgeline",
<Input width: "40%",
//ref={searchInput} ...getColumnSearchProps("bridgeline"),
placeholder={`Search ${dataIndex}`} },
value={selectedKeys[0]}
onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => handleSearch(selectedKeys as string[], confirm, dataIndex)}
style={{ marginBottom: 8, display: 'block' }}
/>
<Space>
<Button
type="primary"
onClick={() => handleSearch(selectedKeys as string[], confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{ width: 90 }}
>
Search
</Button>
<Button
onClick={() => clearFilters && handleReset(clearFilters)}
size="small"
style={{ width: 90 }}
>
Reset
</Button>
<Button
type="link"
size="small"
onClick={() => {
confirm({ closeDropdown: false });
setSearchText((selectedKeys as string[])[0]);
setSearchedColumn(dataIndex);
}}
>
Filter
</Button>
<Button
type="link"
size="small"
onClick={() => {
close();
}}
>
close
</Button>
</Space>
</div>
),
filterIcon: (filtered: boolean) => (
<SearchOutlined style={{ color: filtered ? '#1677ff' : undefined }} />
),
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 ? (
<Highlighter
highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
searchWords={[searchText]}
autoEscape
textToHighlight={text ? text.toString() : ''}
/>
) : (
text
),
});
const bridgesColumns: TableColumnsType<BridgeDataType> = [ {
{title: 'Fingerprint', title: "QR Code",
dataIndex: 'fingerprint', width: "40%",
key: 'fingerprint', key: "qrCodeText",
width: '30%', dataIndex: "qrCodeText",
...getColumnSearchProps('fingerprint')}, render: (_text, record) => {
return <QRCode value={record.qrCodeText} />;
},
},
];
{title: 'Bridgeline', return (
dataIndex: 'bridgeline', <>
key: 'bridgeline', <Table<BridgeDataType>
width: '40%', columns={bridgesColumns}
...getColumnSearchProps('bridgeline')}, dataSource={bridgeLines}
/>
;
</>
);
};
{title: 'QR Code', export default Bridges;
width: '40%',
key: 'qrCodeText',
dataIndex: 'qrCodeText',
render: (_text, record) => {return (<QRCode value={record.qrCodeText} />)}
},
]
return(
<>
<Table<BridgeDataType> columns={bridgesColumns} dataSource={bridgeLines} />;
</>);
}
export default Bridges;

View file

@ -1,187 +1,254 @@
// src/Posts.jsx // src/Posts.jsx
import {useUserContext} from './hooks/UserContext.ts'; import { useUserContext } from "./hooks/UserContext.ts";
import { Button, Form, Input } from 'antd'; import { Button, Form, Input } from "antd";
//import React, {useEffect, useState} from "react"; //import React, {useEffect, useState} from "react";
import {useAuth} from "react-oidc-context"; import { useAuth } from "react-oidc-context";
import {RefreshContext} from "./hooks/RefreshContext.ts"; import { RefreshContext } from "./hooks/RefreshContext.ts";
import {useContext, useState} from "react"; import { useContext, useState } from "react";
import {useOrgContext} from "./hooks/OrgContext.ts"; import { useOrgContext } from "./hooks/OrgContext.ts";
const CreateOrgStep1 = () => { const CreateOrgStep1 = () => {
const {currentUser} = useUserContext(); const { currentUser } = useUserContext();
const [form] = Form.useForm(); const [form] = Form.useForm();
const auth = useAuth(); const auth = useAuth();
const {refreshKey, setRefreshKey} = useContext(RefreshContext); const { refreshKey, setRefreshKey } = useContext(RefreshContext);
const {currentOrg, setCurrentOrg} = useOrgContext(); const { currentOrg, setCurrentOrg } = useOrgContext();
console.log('context value:', {currentOrg, setCurrentOrg}); console.log("context value:", { currentOrg, setCurrentOrg });
const {TextArea} = Input; const { TextArea } = Input;
const [currentStep, setCurrentStep] = useState(1); const [currentStep, setCurrentStep] = useState(1);
async function submitOrgBasicData(values: FormData) { async function submitOrgBasicData(values: FormData) {
if (!auth.user) { if (!auth.user) {
return return;
}
try {
const [name, q1, q2] = Object.values(values);
const data = {
"name": name,
"intake_questionnaire": {
"question_one": q1,
"question_two": q2,
}
}
const token = auth.user.access_token;
const requestOptions = {
method: 'POST',
body: JSON.stringify(data),
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
const response_org = await fetch("/api/v1/org", requestOptions);
const orgID = await response_org.json();
const response_currentOrg = await fetch("/api/v1/org?org_id=" + orgID['id'], {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const currentOrgData = await response_currentOrg.json()
setCurrentOrg(currentOrgData['organisations'][0]);
setCurrentStep(currentStep + 1);
} catch (e) {
console.error(e);
}
} }
try {
const [name, q1, q2] = Object.values(values);
const data = {
name: name,
intake_questionnaire: {
question_one: q1,
question_two: q2,
},
};
async function submitOrgBillingData(values: FormData) { const token = auth.user.access_token;
if (!auth.user) { const requestOptions = {
return method: "POST",
} body: JSON.stringify(data),
try { headers: {
const [first_name, last_name, email, phone_number, vat_number, street_address, street_address_line_2, locality] = Object.values(values); Authorization: `Bearer ${token}`,
const data = { "Content-Type": "application/json",
"organisation_id": currentOrg.organisation_id, },
"contact_type": "billing", };
"first_name": first_name, const response_org = await fetch("/api/v1/org", requestOptions);
"last_name": last_name, const orgID = await response_org.json();
"email": email, const response_currentOrg = await fetch(
"phone_number": phone_number, "/api/v1/org?org_id=" + orgID["id"],
"vat_number": vat_number, {
"street_address": street_address, headers: {
"street_address_line_2": street_address_line_2, Authorization: `Bearer ${token}`,
"locality": locality, "Content-Type": "application/json",
} },
},
const token = auth.user.access_token; );
const requestOptions = { const currentOrgData = await response_currentOrg.json();
method: 'PATCH', setCurrentOrg(currentOrgData["organisations"][0]);
body: JSON.stringify(data), setCurrentStep(currentStep + 1);
headers: { } catch (e) {
Authorization: `Bearer ${token}`, console.error(e);
'Content-Type': 'application/json'
}
}
await fetch("/api/v1/org/contact", requestOptions);
setCurrentStep(currentStep + 1);
setRefreshKey(refreshKey +1);
} catch (e) {
console.error(e);
}
} }
}
if (!currentUser) { async function submitOrgBillingData(values: FormData) {
return <div>Loading...</div>; if (!auth.user) {
return;
} }
try {
const [
first_name,
last_name,
email,
phone_number,
vat_number,
street_address,
street_address_line_2,
locality,
] = Object.values(values);
const data = {
organisation_id: currentOrg.organisation_id,
contact_type: "billing",
first_name: first_name,
last_name: last_name,
email: email,
phone_number: phone_number,
vat_number: vat_number,
street_address: street_address,
street_address_line_2: street_address_line_2,
locality: locality,
};
if (currentStep === 1) { const token = auth.user.access_token;
return ( const requestOptions = {
<> method: "PATCH",
<h2>Step 1 - Basic info</h2> body: JSON.stringify(data),
<Form headers: {
layout="vertical" Authorization: `Bearer ${token}`,
form={form} "Content-Type": "application/json",
onFinish={submitOrgBasicData} },
> };
<Form.Item name="Organisation name" label="Organisation name" rules={[{required: true}]}> await fetch("/api/v1/org/contact", requestOptions);
<Input/> setCurrentStep(currentStep + 1);
</Form.Item> setRefreshKey(refreshKey + 1);
<h4>Intake Questionnaire</h4> } catch (e) {
<Form.Item name="Question 1" label="What region(s) does your organisation operate in?"> console.error(e);
<TextArea showCount maxLength={100} placeholder="e.g., Southern Africa"/>
</Form.Item>
<Form.Item name="Question 2" label="Briefly describe how your organisation supports civil society.">
<TextArea showCount maxLength={300}
placeholder="e.g., Our organisation supports various groups such as ..."/>
</Form.Item>
<Form.Item name="Question 3" label="Name two references/contacts within the community, and briefly describe the nature of your relationsip and projects you've worked on together.">
<TextArea showCount maxLength={500}
placeholder="e.g., I worked with SR2 Communications on providing digital security training from 2023-2025."/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">Submit</Button>
</Form.Item>
</Form>
</>
);
} }
}
if (currentStep === 2) { if (!currentUser) {
return <> return <div>Loading...</div>;
<h2>Step 2 - Contact and billing details</h2> }
<Form if (currentStep === 1) {
layout="vertical" return (
form={form} <>
onFinish={submitOrgBillingData} <h2>Step 1 - Basic info</h2>
> <Form layout="vertical" form={form} onFinish={submitOrgBasicData}>
<Form.Item name="Billing contact first name" label="Billing contact first name" <Form.Item
rules={[{required: true}]}> name="Organisation name"
<Input/> label="Organisation name"
</Form.Item> rules={[{ required: true }]}
<Form.Item name="Billing contact last name" label="Billing contact last name" >
rules={[{required: true}]}> <Input />
<Input/> </Form.Item>
</Form.Item> <h4>Intake Questionnaire</h4>
<Form.Item name="Billing contact email" label="Billing contact email address" <Form.Item
rules={[{required: true, type: "email", message: "Not a valid e-mail address."}]}> name="Question 1"
<Input/> label="What region(s) does your organisation operate in?"
</Form.Item> >
<Form.Item name="Billing contact phone" label="Billing contact phone number"> <TextArea
<Input/> showCount
</Form.Item> maxLength={100}
<Form.Item name="Billing contact VAT number" label="Billing contact VAT number"> placeholder="e.g., Southern Africa"
<Input/> />
</Form.Item> </Form.Item>
<Form.Item name="Billing address line 1" label="Billing address line 1" rules={[{required: true}]}> <Form.Item
<Input/> name="Question 2"
</Form.Item> label="Briefly describe how your organisation supports civil society."
<Form.Item name="Billing address line 2" label="Billing address line 2"> >
<Input/> <TextArea
</Form.Item> showCount
<Form.Item name="locality" label="City" rules={[{required: true}]}> maxLength={300}
<Input/> placeholder="e.g., Our organisation supports various groups such as ..."
</Form.Item> />
<Form.Item> </Form.Item>
<Button type="primary" htmlType="submit">Submit</Button> <Form.Item
</Form.Item> name="Question 3"
</Form> label="Name two references/contacts within the community, and briefly describe the nature of your relationsip and projects you've worked on together."
</> >
} <TextArea
showCount
maxLength={500}
placeholder="e.g., I worked with SR2 Communications on providing digital security training from 2023-2025."
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</>
);
}
if (currentStep === 3) { if (currentStep === 2) {
return <> <h2>Thank you for submitting your organisation info. </h2> return (
<p> This will be manually reviewed by a member of SR2 Cloud and you will be contacted shortly. <>
We aim to complete reviews within 24 hours. You can change your answers at any point from the <h2>Step 2 - Contact and billing details</h2>
'My organisations' section.</p>
</>
}
}
export default CreateOrgStep1; <Form layout="vertical" form={form} onFinish={submitOrgBillingData}>
<Form.Item
name="Billing contact first name"
label="Billing contact first name"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
name="Billing contact last name"
label="Billing contact last name"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
name="Billing contact email"
label="Billing contact email address"
rules={[
{
required: true,
type: "email",
message: "Not a valid e-mail address.",
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="Billing contact phone"
label="Billing contact phone number"
>
<Input />
</Form.Item>
<Form.Item
name="Billing contact VAT number"
label="Billing contact VAT number"
>
<Input />
</Form.Item>
<Form.Item
name="Billing address line 1"
label="Billing address line 1"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
name="Billing address line 2"
label="Billing address line 2"
>
<Input />
</Form.Item>
<Form.Item name="locality" label="City" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</>
);
}
if (currentStep === 3) {
return (
<>
{" "}
<h2>Thank you for submitting your organisation info. </h2>
<p>
{" "}
This will be manually reviewed by a member of SR2 Cloud and you will
be contacted shortly. We aim to complete reviews within 24 hours. You
can change your answers at any point from the 'My organisations'
section.
</p>
</>
);
}
};
export default CreateOrgStep1;

View file

@ -1,43 +1,60 @@
import {Breadcrumb, Button, theme} from "antd"; import { Breadcrumb, Button, theme } from "antd";
import React from "react"; import React from "react";
import {useUserContext} from "./hooks/UserContext.ts"; import { useUserContext } from "./hooks/UserContext.ts";
import {useNavigate} from "react-router"; import { useNavigate } from "react-router";
import Organisations from "./Organisations.tsx"; import Organisations from "./Organisations.tsx";
const Home: React.FC = () => { const Home: React.FC = () => {
const { const {
token: { colorBgContainer, borderRadiusLG }, token: { colorBgContainer, borderRadiusLG },
} = theme.useToken(); } = theme.useToken();
const navigate = useNavigate(); const navigate = useNavigate();
const { currentUser } = useUserContext(); const { currentUser } = useUserContext();
if (currentUser.organisations.length === 0) { if (currentUser.organisations.length === 0) {
return <> return (
<h2>Welcome to SR2 Cloud! </h2> <>
<h3>If your organisation already uses SR2 Cloud, please ask your administrator to add you to the organisation.</h3> <h2>Welcome to SR2 Cloud! </h2>
<p> If your organanisation is new to SR2 Cloud, start the onboarding process </p> <h3>
If your organisation already uses SR2 Cloud, please ask your
administrator to add you to the organisation.
</h3>
<p>
{" "}
If your organanisation is new to SR2 Cloud, start the onboarding
process{" "}
</p>
<Button type="primary" onClick={() => navigate('/create')}> Start onboarding </Button> <Button type="primary" onClick={() => navigate("/create")}>
</> {" "}
} Start onboarding{" "}
</Button>
</>
);
}
//TODO: fix this breadcrumb //TODO: fix this breadcrumb
return (<> return (
<Breadcrumb style={{ margin: '16px 0' }} items={[{ title: 'User' }, { title: 'Dashboard' }]} /> <>
<div <Breadcrumb
style={{ margin: "16px 0" }}
items={[{ title: "User" }, { title: "Dashboard" }]}
/>
<div
style={{ style={{
padding: 24, padding: 24,
minHeight: 360, minHeight: 360,
background: colorBgContainer, background: colorBgContainer,
borderRadius: borderRadiusLG, borderRadius: borderRadiusLG,
}} }}
> >
<h1>Welcome to your dashboard, {currentUser.first_name}!</h1> <h1>Welcome to your dashboard, {currentUser.first_name}!</h1>
<div> <div>
<Organisations/> <Organisations />
</div> </div>
</div> </div>
</>) </>
} );
};
export default Home; export default Home;

View file

@ -1,76 +1,90 @@
import {Button, Card, Space} from 'antd'; import { Button, Card, Space } from "antd";
import type {OrgObject} from "./hooks/OrgContext.ts"; import type { OrgObject } from "./hooks/OrgContext.ts";
import {useUserContext} from "./hooks/UserContext.ts"; import { useUserContext } from "./hooks/UserContext.ts";
import {useAuth} from "react-oidc-context"; import { useAuth } from "react-oidc-context";
import {useContext} from "react"; import { useContext } from "react";
import {RefreshContext} from "./hooks/RefreshContext.ts"; import { RefreshContext } from "./hooks/RefreshContext.ts";
const OrgPanel = (props: OrgObject) => { const OrgPanel = (props: OrgObject) => {
const auth = useAuth();
const auth = useAuth(); const { refreshKey, setRefreshKey } = useContext(RefreshContext);
const { refreshKey, setRefreshKey } = useContext(RefreshContext); const { currentUser } = useUserContext();
const {currentUser} = useUserContext(); async function removeUserFromOrg(org_id: number) {
async function removeUserFromOrg(org_id: number) { if (!auth.user) {
if (!auth.user) { return;
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);
}
} }
try {
const token = auth.user.access_token;
async function removeOrg(org_id: number) { const requestOptions = {
if (!auth.user) { method: "DELETE",
return headers: {
} Authorization: `Bearer ${token}`,
try { "Content-Type": "application/json",
const token = auth.user.access_token; },
const requestOptions = { };
method: 'DELETE', const response = await fetch(
headers: {Authorization: `Bearer ${token}`, "/api/v1/org/user?org_id=" + org_id + "&user_id=" + currentUser.id,
'Content-Type': 'application/json'} requestOptions,
} );
await fetch("/api/v1/org/self?org_id=" + org_id, requestOptions); await response.json();
setRefreshKey(prev => prev + 1); setRefreshKey((prev) => prev + 1);
console.log(refreshKey) console.log(refreshKey);
} catch (e) {
} catch (e) { console.error(e);
console.error(e);
}
} }
}
return ( async function removeOrg(org_id: number) {
<Space vertical size={16}> if (!auth.user) {
<Card title={props.name} extra={<a href="#">More</a>} style={{width: 300}}> return;
<p>Validated? {props.status}</p> }
<p>Root User: {props.root_user_email}</p> try {
<p>Billing Contact: {props.billing_contact?.email || 'Not set'}</p> const token = auth.user.access_token;
<h3>Questionnaire</h3> const requestOptions = {
<p>Q1: {props.intake_questionnaire?.question_one || 'Not set'}</p> method: "DELETE",
<p>Q2: {props.intake_questionnaire?.question_two || 'Not set'}</p> headers: {
<p>Q3: {props.intake_questionnaire?.question_three || 'Not set'}</p> 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);
}
}
<Button type="primary" onClick={() => removeUserFromOrg(props.organisation_id)}> return (
Remove yourself from organisation <Space vertical size={16}>
</Button> <Card
<Button type="primary" danger onClick={() => removeOrg(props.organisation_id)}> title={props.name}
Remove organisation extra={<a href="#">More</a>}
</Button> style={{ width: 300 }}
</Card> >
</Space> <p>Validated? {props.status}</p>
); <p>Root User: {props.root_user_email}</p>
} <p>Billing Contact: {props.billing_contact?.email || "Not set"}</p>
export default OrgPanel; <h3>Questionnaire</h3>
<p>Q1: {props.intake_questionnaire?.question_one || "Not set"}</p>
<p>Q2: {props.intake_questionnaire?.question_two || "Not set"}</p>
<p>Q3: {props.intake_questionnaire?.question_three || "Not set"}</p>
<Button
type="primary"
onClick={() => removeUserFromOrg(props.organisation_id)}
>
Remove yourself from organisation
</Button>
<Button
type="primary"
danger
onClick={() => removeOrg(props.organisation_id)}
>
Remove organisation
</Button>
</Card>
</Space>
);
};
export default OrgPanel;

View file

@ -1,48 +1,50 @@
import React, {useState} from "react"; import React, { useState } from "react";
import OrgPanel from "./OrgPanel"; import OrgPanel from "./OrgPanel";
import {useAuth} from "react-oidc-context"; import { useAuth } from "react-oidc-context";
import type {OrgObject} from "./hooks/OrgContext.ts"; import type { OrgObject } from "./hooks/OrgContext.ts";
import type { JSX } from "react/jsx-runtime"; import type { JSX } from "react/jsx-runtime";
import {useRefreshContext} from "./hooks/RefreshContext.ts"; import { useRefreshContext } from "./hooks/RefreshContext.ts";
const Organisations: React.FC = () => { const Organisations: React.FC = () => {
const auth = useAuth(); const auth = useAuth();
const [orgData, setCurrentOrgData] = useState<OrgObject[]>(); const [orgData, setCurrentOrgData] = useState<OrgObject[]>();
const { refreshKey } = useRefreshContext(); const { refreshKey } = useRefreshContext();
React.useEffect(() => { React.useEffect(() => {
const fetchOrgData = async () => { const fetchOrgData = async () => {
console.log("Fetching org data"); console.log("Fetching org data");
if (!auth.user) return if (!auth.user) return;
try { try {
const requestOptions = { const requestOptions = {
method: 'GET', method: "GET",
headers: { headers: {
Authorization: `Bearer ${auth.user.access_token}`, Authorization: `Bearer ${auth.user.access_token}`,
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
} };
const response = await fetch("/api/v1/user/self/orgs", requestOptions); const response = await fetch("/api/v1/user/self/orgs", requestOptions);
const orgsObject = await response.json(); const orgsObject = await response.json();
setCurrentOrgData(orgsObject['organisations']) setCurrentOrgData(orgsObject["organisations"]);
} catch (e) {
console.error(e);
}
};
fetchOrgData().catch(console.error);
}, [auth.user, refreshKey]);
} catch (e) { console.log(orgData);
console.error(e);
}
}
fetchOrgData()
.catch(console.error);
}, [auth.user, refreshKey]);
console.log(orgData); let panels: JSX.Element[] = [];
if (orgData) {
panels = orgData.map((org) => (
<OrgPanel {...org} key={org.organisation_id} />
));
}
return (
<>
<h1>Your organisations</h1>
{panels}
</>
);
};
let panels: JSX.Element[] = []; export default Organisations;
if (orgData) {
panels = orgData.map(org => <OrgPanel {...org} key={org.organisation_id} />);
}
return(<>
<h1>Your organisations</h1>
{panels}
</>);
}
export default Organisations;

View file

@ -1,23 +1,25 @@
// src/Posts.jsx // src/Posts.jsx
import { useUserContext} from './hooks/UserContext.ts'; import { useUserContext } from "./hooks/UserContext.ts";
const Profile = () => { const Profile = () => {
const { currentUser } = useUserContext(); const { currentUser } = useUserContext();
if (!currentUser) { if (!currentUser) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
console.log(currentUser); console.log(currentUser);
console.log(currentUser.organisations); console.log(currentUser.organisations);
const userOrgs: { name: string, id: number }[] = currentUser.organisations; const userOrgs: { name: string; id: number }[] = currentUser.organisations;
return ( return (
<> <>
<p>User: {currentUser.first_name} {currentUser.last_name}</p> <p>
<p>Email: {currentUser.email}</p> User: {currentUser.first_name} {currentUser.last_name}
{userOrgs[0] && <p>Current Orgs: {userOrgs[0].name}</p>} </p>
</> <p>Email: {currentUser.email}</p>
); {userOrgs[0] && <p>Current Orgs: {userOrgs[0].name}</p>}
</>
);
}; };
export default Profile; export default Profile;

View file

@ -1,36 +1,36 @@
import React, {createContext, useContext} from 'react'; import React, { createContext, useContext } from "react";
export interface Contact { export interface Contact {
email: string; email: string;
id: number; id: number;
} }
export interface OrgObject { export interface OrgObject {
name: string, name: string;
status: "partial", status: "partial";
root_user_email: string, root_user_email: string;
billing_contact: Contact, billing_contact: Contact;
owner_contact: Contact, owner_contact: Contact;
security_contact:Contact, security_contact: Contact;
organisation_id: number, organisation_id: number;
intake_questionnaire: { intake_questionnaire: {
question_one: string, question_one: string;
question_two: string, question_two: string;
question_three: string question_three: string;
}, };
} }
export interface OrgContextType { export interface OrgContextType {
currentOrg: OrgObject; currentOrg: OrgObject;
setCurrentOrg: React.Dispatch<React.SetStateAction<OrgObject>>; setCurrentOrg: React.Dispatch<React.SetStateAction<OrgObject>>;
} }
export const OrgContext = createContext<OrgContextType | undefined>(undefined); export const OrgContext = createContext<OrgContextType | undefined>(undefined);
export const useOrgContext = () => { export const useOrgContext = () => {
const context: OrgContextType | undefined = useContext(OrgContext); const context: OrgContextType | undefined = useContext(OrgContext);
if (context === undefined) { if (context === undefined) {
throw new Error('Organisation context not found'); throw new Error("Organisation context not found");
} }
return context; return context;
}; };

View file

@ -1,19 +1,19 @@
import React, {createContext, useContext} from 'react'; import React, { createContext, useContext } from "react";
export interface RefreshType { export interface RefreshType {
refreshKey: number; refreshKey: number;
setRefreshKey: React.Dispatch<React.SetStateAction<number>>; setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
} }
export const RefreshContext = createContext<RefreshType>({ export const RefreshContext = createContext<RefreshType>({
refreshKey: 1, refreshKey: 1,
setRefreshKey: () => {}, setRefreshKey: () => {},
}); });
export const useRefreshContext = () => { export const useRefreshContext = () => {
const context: RefreshType | undefined = useContext(RefreshContext); const context: RefreshType | undefined = useContext(RefreshContext);
if (context === undefined) { if (context === undefined) {
throw new Error('Organisation refresh context not found'); throw new Error("Organisation refresh context not found");
} }
return context; return context;
}; };

View file

@ -1,27 +1,26 @@
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from "react";
export interface userObject { export interface userObject {
first_name: string; first_name: string;
last_name: string; last_name: string;
email: string; email: string;
organisations: { name: string, id: number }[]; organisations: { name: string; id: number }[];
id: number; id: number;
} }
export interface UserContextType { export interface UserContextType {
currentUser: userObject; currentUser: userObject;
setCurrentUser: React.Dispatch<React.SetStateAction<userObject>>; setCurrentUser: React.Dispatch<React.SetStateAction<userObject>>;
} }
export const UserContext = createContext<UserContextType | undefined>(undefined); export const UserContext = createContext<UserContextType | undefined>(
undefined,
);
export const useUserContext = () => { export const useUserContext = () => {
const context: UserContextType | undefined = useContext(UserContext); const context: UserContextType | undefined = useContext(UserContext);
if (context === undefined) { if (context === undefined) {
throw new Error('User context not found!'); throw new Error("User context not found!");
} }
return context; return context;
}; };

View file

@ -1,27 +1,29 @@
import {StrictMode} from 'react' import { StrictMode } from "react";
import {createRoot} from 'react-dom/client' import { createRoot } from "react-dom/client";
import './index.css' import "./index.css";
import App from './App.tsx' import App from "./App.tsx";
import {AuthProvider, type AuthProviderProps} from "react-oidc-context"; import { AuthProvider, type AuthProviderProps } from "react-oidc-context";
import {type User, WebStorageStateStore} from "oidc-client-ts"; import { type User, WebStorageStateStore } from "oidc-client-ts";
import {BrowserRouter} from "react-router"; import { BrowserRouter } from "react-router";
const onSigninCallback = (_user: User | void): void => { const onSigninCallback = (_user: User | void): void => {
window.history.replaceState({}, document.title, window.location.pathname) window.history.replaceState({}, document.title, window.location.pathname);
}
const oidcConfig: AuthProviderProps = {
authority: "https://sso.sr2.uk/realms/sr2",
client_id: "chris-dev",
redirect_uri: window.location.href,
onSigninCallback: onSigninCallback,
userStore: new WebStorageStateStore({store: window.localStorage}),
}; };
createRoot(document.getElementById('root')!).render(<StrictMode> const oidcConfig: AuthProviderProps = {
authority: "https://sso.sr2.uk/realms/sr2",
client_id: "chris-dev",
redirect_uri: window.location.href,
onSigninCallback: onSigninCallback,
userStore: new WebStorageStateStore({ store: window.localStorage }),
};
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AuthProvider {...oidcConfig}> <AuthProvider {...oidcConfig}>
<BrowserRouter basename="/ui"> <BrowserRouter basename="/ui">
<App/> <App />
</BrowserRouter> </BrowserRouter>
</AuthProvider> </AuthProvider>
</StrictMode>,) </StrictMode>,
);

View file

@ -1,23 +1,23 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
base: '/ui/', base: "/ui/",
plugins: [react()], plugins: [react()],
server: { server: {
proxy: { proxy: {
'/api': { "/api": {
target: 'http://localhost:8000', target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
ws: true, ws: true,
configure: (proxy, _options) => { configure: (proxy, _options) => {
proxy.on('error', err => { proxy.on("error", (err) => {
console.error(err.message) console.error(err.message);
}) });
} },
} },
} },
} },
}) });