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
export default defineConfig([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
@ -34,40 +34,40 @@ export default defineConfig([
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// 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:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";
export default defineConfig([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
reactX.configs["recommended-typescript"],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
]);
```

View file

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

View file

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

View file

@ -1,31 +1,33 @@
import './App.css'
import "./App.css";
import { useAuth, hasAuthParams } from "react-oidc-context";
import React, {useState} from 'react';
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';
} 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 { Route, Routes, useLocation, useNavigate } from "react-router";
import Profile from "./Profile.tsx";
import {type UserContextType} from './hooks/UserContext.ts';
import {UserContext} from './hooks/UserContext.ts';
import type {userObject} from "./hooks/UserContext.ts";
import { type UserContextType } from "./hooks/UserContext.ts";
import { UserContext } from "./hooks/UserContext.ts";
import type { userObject } from "./hooks/UserContext.ts";
import Organisations from "./Organisations.tsx";
import {RefreshContext} from "./hooks/RefreshContext.ts";
import { RefreshContext } from "./hooks/RefreshContext.ts";
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";
const { Header, Content, Sider } = Layout;
type MenuItem = Required<MenuProps>['items'][number];
type MenuItem = Required<MenuProps>["items"][number];
function getItem(
label: React.ReactNode,
@ -41,12 +43,11 @@ function getItem(
} as MenuItem;
}
const App: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const {
token: {colorBgContainer, borderRadiusLG},
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();
/*******************
SIGNING IN WITH OIDC
@ -55,8 +56,11 @@ const App: React.FC = () => {
const hasTriedSignin = React.useRef(false);
// automatically sign-in
React.useEffect(() => {
if (!hasAuthParams() &&
!auth.isAuthenticated && !auth.activeNavigator && !auth.isLoading &&
if (
!hasAuthParams() &&
!auth.isAuthenticated &&
!auth.activeNavigator &&
!auth.isLoading &&
!hasTriedSignin.current
) {
auth.signinRedirect();
@ -70,19 +74,19 @@ const App: React.FC = () => {
last_name: "",
email: "",
organisations: [],
id: -1
id: -1,
},
setCurrentUser: () => {}
}
setCurrentUser: () => {},
};
const defaultOrg: OrgContextType = {
currentOrg: {
name: "",
status: "partial",
root_user_email: "",
billing_contact: {email: "", id: -1},
owner_contact: {email: "", id: -1},
security_contact: {email: "", id: -1},
billing_contact: { email: "", id: -1 },
owner_contact: { email: "", id: -1 },
security_contact: { email: "", id: -1 },
organisation_id: -1,
intake_questionnaire: {
question_one: "",
@ -90,13 +94,16 @@ const App: React.FC = () => {
question_three: "",
},
},
setCurrentOrg: () => {}
}
setCurrentOrg: () => {},
};
const [currentUser, setCurrentUser] = useState<userObject>(defaultUser.currentUser);
const [currentUser, setCurrentUser] = useState<userObject>(
defaultUser.currentUser,
);
const [refreshKey, setRefreshKey] = useState<number>(1);
const [currentOrg, setCurrentOrg] = useState<OrgObject>(defaultOrg.currentOrg);
const [currentOrg, setCurrentOrg] = useState<OrgObject>(
defaultOrg.currentOrg,
);
/*****************************
GETTING CURRENT USER FROM API
@ -105,7 +112,9 @@ const App: React.FC = () => {
React.useEffect(() => {
(async () => {
if (!auth.user) { return }
if (!auth.user) {
return;
}
try {
const token = auth.user.access_token;
const response = await fetch("/api/v1/user/self/db", {
@ -120,9 +129,8 @@ const App: React.FC = () => {
})();
}, [auth, refreshKey]);
if (!currentUser) {
return(
return (
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
<div>Determining your existing user...</div>;
</UserContext.Provider>
@ -130,7 +138,7 @@ const App: React.FC = () => {
}
if (auth.isLoading) {
return(
return (
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
<div>Signing you in/out...</div>;
</UserContext.Provider>
@ -138,49 +146,53 @@ const App: React.FC = () => {
}
if (!auth.isAuthenticated) {
return(
return (
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
<div>Unable to log in</div>;
</UserContext.Provider>
);
}
const onClick: MenuProps['onClick'] = (e) => {
const onClick: MenuProps["onClick"] = (e) => {
switch (e.key) {
case 'profile':
navigate('/profile');
case "profile":
navigate("/profile");
break;
case 'orgs':
navigate('/create-org-flow');
case "orgs":
navigate("/create-org-flow");
break;
case 'logout':
auth.removeUser().then(() => navigate('/profile'));
case "logout":
auth.removeUser().then(() => navigate("/profile"));
break;
case 'bridges':
navigate('/bridges');
case "bridges":
navigate("/bridges");
break;
default:
console.log('Clicked item:', e.key);
console.log("Clicked item:", e.key);
}
};
let side_nav_items: MenuItem[] = []
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')]),
getItem(
currentUser.first_name + " " + currentUser.last_name,
"account",
<UserOutlined />,
[getItem("Account details", "profile"), getItem("Log out", "logout")],
),
];
if (currentUser.organisations.length > 0) {
side_nav_items = [
getItem("Onboarding", "orgs", <SettingOutlined />),
getItem("Bridges", "bridges", <DeploymentUnitOutlined />),
];
side_nav_items = [ getItem('Onboarding', 'orgs', <SettingOutlined />),
getItem('Bridges', 'bridges', <DeploymentUnitOutlined />), ];
const currentOrg = currentUser.organisations[0]['name']
top_nav_items.push(getItem(currentOrg +" Settings", 'orgsettings', <SettingOutlined />));
const currentOrg = currentUser.organisations[0]["name"];
top_nav_items.push(
getItem(currentOrg + " Settings", "orgsettings", <SettingOutlined />),
);
}
return (
@ -188,34 +200,37 @@ const App: React.FC = () => {
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
<RefreshContext.Provider value={{ refreshKey, setRefreshKey }}>
<Layout>
<Header style={{ display: 'flex', alignItems: 'right' }}>
<div className="demo-logo" >
<Header style={{ display: "flex", alignItems: "right" }}>
<div className="demo-logo">
<h1>SR22</h1>
</div>
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={['2']}
defaultSelectedKeys={["2"]}
items={top_nav_items}
onClick={onClick}
style={{ flex: 1, minWidth: 0, justifyContent:"flex-end" }}
style={{ flex: 1, minWidth: 0, justifyContent: "flex-end" }}
/>
</Header>
<Layout>
<Sider width={200} style={{ background: colorBgContainer }}>
<Menu
mode="inline"
defaultSelectedKeys={['1']}
defaultOpenKeys={['sub1']}
style={{ height: '100%', borderInlineEnd: 0 }}
defaultSelectedKeys={["1"]}
defaultOpenKeys={["sub1"]}
style={{ height: "100%", borderInlineEnd: 0 }}
items={side_nav_items}
onClick={onClick}
/>
</Sider>
<Layout style={{ padding: '0 24px 24px' }}>
<Layout style={{ padding: "0 24px 24px" }}>
<Breadcrumb
items={[{ title: 'Home' }, { title: location.pathname.substring(1) }]}
style={{ margin: '16px 0' }}
items={[
{ title: "Home" },
{ title: location.pathname.substring(1) },
]}
style={{ margin: "16px 0" }}
/>
<Content
style={{
@ -226,15 +241,24 @@ const App: React.FC = () => {
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/>}/> }
{currentUser && <Route path="/bridges" element={<Bridges/>}/> }
<Route path="/" element={<Home />} />
{currentUser && (
<Route path="/profile" element={<Profile />} />
)}
{currentUser && (
<Route
path="/organisations"
element={<Organisations />}
/>
)}
{currentUser && (
<Route path="/create" element={<CreateOrgFlow />} />
)}
{currentUser && (
<Route path="/bridges" element={<Bridges />} />
)}
</Routes>
</Content>
</Layout>
@ -243,10 +267,7 @@ const App: React.FC = () => {
</RefreshContext.Provider>
</UserContext.Provider>
</OrgContext.Provider>
);
};
}
export default App
export default App;

View file

@ -1,10 +1,10 @@
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 } from 'antd';
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 } from "antd";
interface BridgeDataType {
fingerprint: string;
bridgeline: string;
@ -12,29 +12,49 @@ interface BridgeDataType {
}
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: ""},
{
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)});
bridgeLines.forEach((line) => {
line.qrCodeText = makeQRCodeText(line.bridgeline);
});
console.log(bridgeLines);
const [searchText, setSearchText] = useState('');
const [searchedColumn, setSearchedColumn] = useState('');
const [searchText, setSearchText] = useState("");
const [searchedColumn, setSearchedColumn] = useState("");
const searchInput = useRef<InputRef>(null);
const handleSearch = (
selectedKeys: string[],
confirm: FilterDropdownProps['confirm'],
confirm: FilterDropdownProps["confirm"],
dataIndex: DataIndex,
) => {
confirm();
@ -44,25 +64,39 @@ const Bridges = () => {
const handleReset = (clearFilters: () => void) => {
clearFilters();
setSearchText('');
setSearchText("");
};
type DataIndex = keyof BridgeDataType;
const getColumnSearchProps = (dataIndex: DataIndex): TableColumnType<BridgeDataType> => ({
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => (
const getColumnSearchProps = (
dataIndex: DataIndex,
): TableColumnType<BridgeDataType> => ({
filterDropdown: ({
setSelectedKeys,
selectedKeys,
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' }}
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)}
onClick={() =>
handleSearch(selectedKeys as string[], confirm, dataIndex)
}
icon={<SearchOutlined />}
size="small"
style={{ width: 90 }}
@ -100,7 +134,7 @@ const Bridges = () => {
</div>
),
filterIcon: (filtered: boolean) => (
<SearchOutlined style={{ color: filtered ? '#1677ff' : undefined }} />
<SearchOutlined style={{ color: filtered ? "#1677ff" : undefined }} />
),
onFilter: (value, record) =>
record[dataIndex]
@ -117,10 +151,10 @@ const Bridges = () => {
render: (text) =>
searchedColumn === dataIndex ? (
<Highlighter
highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
highlightStyle={{ backgroundColor: "#ffc069", padding: 0 }}
searchWords={[searchText]}
autoEscape
textToHighlight={text ? text.toString() : ''}
textToHighlight={text ? text.toString() : ""}
/>
) : (
text
@ -128,34 +162,42 @@ const Bridges = () => {
});
const bridgesColumns: TableColumnsType<BridgeDataType> = [
{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 (<QRCode value={record.qrCodeText} />)}
{
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 <QRCode value={record.qrCodeText} />;
},
},
];
return(
return (
<>
<Table<BridgeDataType> columns={bridgesColumns} dataSource={bridgeLines} />;
</>);
}
<Table<BridgeDataType>
columns={bridgesColumns}
dataSource={bridgeLines}
/>
;
</>
);
};
export default Bridges;

View file

@ -1,59 +1,61 @@
// src/Posts.jsx
import {useUserContext} from './hooks/UserContext.ts';
import { Button, Form, Input } from 'antd';
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, useState} from "react";
import {useOrgContext} from "./hooks/OrgContext.ts";
import { useAuth } from "react-oidc-context";
import { RefreshContext } from "./hooks/RefreshContext.ts";
import { useContext, useState } from "react";
import { useOrgContext } from "./hooks/OrgContext.ts";
const CreateOrgStep1 = () => {
const {currentUser} = useUserContext();
const { currentUser } = useUserContext();
const [form] = Form.useForm();
const auth = useAuth();
const {refreshKey, setRefreshKey} = useContext(RefreshContext);
const {currentOrg, setCurrentOrg} = useOrgContext();
console.log('context value:', {currentOrg, setCurrentOrg});
const { refreshKey, setRefreshKey } = useContext(RefreshContext);
const { currentOrg, setCurrentOrg } = useOrgContext();
console.log("context value:", { currentOrg, setCurrentOrg });
const {TextArea} = Input;
const { TextArea } = Input;
const [currentStep, setCurrentStep] = useState(1);
async function submitOrgBasicData(values: FormData) {
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,
}
}
name: name,
intake_questionnaire: {
question_one: q1,
question_two: q2,
},
};
const token = auth.user.access_token;
const requestOptions = {
method: 'POST',
method: "POST",
body: JSON.stringify(data),
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
"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'], {
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]);
"Content-Type": "application/json",
},
},
);
const currentOrgData = await response_currentOrg.json();
setCurrentOrg(currentOrgData["organisations"][0]);
setCurrentStep(currentStep + 1);
} catch (e) {
console.error(e);
}
@ -61,36 +63,44 @@ const CreateOrgStep1 = () => {
async function submitOrgBillingData(values: FormData) {
if (!auth.user) {
return
return;
}
try {
const [first_name, last_name, email, phone_number, vat_number, street_address, street_address_line_2, locality] = Object.values(values);
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,
}
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,
};
const token = auth.user.access_token;
const requestOptions = {
method: 'PATCH',
method: "PATCH",
body: JSON.stringify(data),
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
"Content-Type": "application/json",
},
};
await fetch("/api/v1/org/contact", requestOptions);
setCurrentStep(currentStep + 1);
setRefreshKey(refreshKey +1);
setRefreshKey(refreshKey + 1);
} catch (e) {
console.error(e);
}
@ -104,28 +114,49 @@ const CreateOrgStep1 = () => {
return (
<>
<h2>Step 1 - Basic info</h2>
<Form
layout="vertical"
form={form}
onFinish={submitOrgBasicData}
<Form layout="vertical" form={form} onFinish={submitOrgBasicData}>
<Form.Item
name="Organisation name"
label="Organisation name"
rules={[{ required: true }]}
>
<Form.Item name="Organisation name" label="Organisation name" rules={[{required: true}]}>
<Input/>
<Input />
</Form.Item>
<h4>Intake Questionnaire</h4>
<Form.Item name="Question 1" label="What region(s) does your organisation operate in?">
<TextArea showCount maxLength={100} placeholder="e.g., Southern Africa"/>
<Form.Item
name="Question 1"
label="What region(s) does your organisation operate in?"
>
<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
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
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>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</>
@ -133,55 +164,91 @@ const CreateOrgStep1 = () => {
}
if (currentStep === 2) {
return <>
return (
<>
<h2>Step 2 - Contact and billing details</h2>
<Form
layout="vertical"
form={form}
onFinish={submitOrgBillingData}
<Form layout="vertical" form={form} onFinish={submitOrgBillingData}>
<Form.Item
name="Billing contact first name"
label="Billing contact first name"
rules={[{ required: true }]}
>
<Form.Item name="Billing contact first name" label="Billing contact first name"
rules={[{required: true}]}>
<Input/>
<Input />
</Form.Item>
<Form.Item name="Billing contact last name" label="Billing contact last name"
rules={[{required: true}]}>
<Input/>
<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
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
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
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
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
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 name="locality" label="City" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">Submit</Button>
<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>
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,7 +1,7 @@
import {Breadcrumb, Button, theme} from "antd";
import { Breadcrumb, Button, theme } from "antd";
import React from "react";
import {useUserContext} from "./hooks/UserContext.ts";
import {useNavigate} from "react-router";
import { useUserContext } from "./hooks/UserContext.ts";
import { useNavigate } from "react-router";
import Organisations from "./Organisations.tsx";
const Home: React.FC = () => {
@ -12,18 +12,34 @@ const Home: React.FC = () => {
const { currentUser } = useUserContext();
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>
<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
return (<>
<Breadcrumb style={{ margin: '16px 0' }} items={[{ title: 'User' }, { title: 'Dashboard' }]} />
return (
<>
<Breadcrumb
style={{ margin: "16px 0" }}
items={[{ title: "User" }, { title: "Dashboard" }]}
/>
<div
style={{
padding: 24,
@ -34,10 +50,11 @@ const Home: React.FC = () => {
>
<h1>Welcome to your dashboard, {currentUser.first_name}!</h1>
<div>
<Organisations/>
<Organisations />
</div>
</div>
</>)
}
</>
);
};
export default Home;

View file

@ -1,52 +1,55 @@
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";
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();
const { currentUser } = useUserContext();
async function removeUserFromOrg(org_id: number) {
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);
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)
setRefreshKey((prev) => prev + 1);
console.log(refreshKey);
} catch (e) {
console.error(e);
}
}
async function removeOrg(org_id: number) {
if (!auth.user) {
return
return;
}
try {
const token = auth.user.access_token;
const requestOptions = {
method: 'DELETE',
headers: {Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'}
}
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)
setRefreshKey((prev) => prev + 1);
console.log(refreshKey);
} catch (e) {
console.error(e);
}
@ -54,23 +57,34 @@ const OrgPanel = (props: OrgObject) => {
return (
<Space vertical size={16}>
<Card title={props.name} extra={<a href="#">More</a>} style={{width: 300}}>
<Card
title={props.name}
extra={<a href="#">More</a>}
style={{ width: 300 }}
>
<p>Validated? {props.status}</p>
<p>Root User: {props.root_user_email}</p>
<p>Billing Contact: {props.billing_contact?.email || 'Not set'}</p>
<p>Billing Contact: {props.billing_contact?.email || "Not set"}</p>
<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>
<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)}>
<Button
type="primary"
onClick={() => removeUserFromOrg(props.organisation_id)}
>
Remove yourself from organisation
</Button>
<Button type="primary" danger onClick={() => removeOrg(props.organisation_id)}>
<Button
type="primary"
danger
onClick={() => removeOrg(props.organisation_id)}
>
Remove organisation
</Button>
</Card>
</Space>
);
}
};
export default OrgPanel;

View file

@ -1,9 +1,9 @@
import React, {useState} from "react";
import React, { useState } from "react";
import OrgPanel from "./OrgPanel";
import {useAuth} from "react-oidc-context";
import type {OrgObject} from "./hooks/OrgContext.ts";
import { useAuth } from "react-oidc-context";
import type { OrgObject } from "./hooks/OrgContext.ts";
import type { JSX } from "react/jsx-runtime";
import {useRefreshContext} from "./hooks/RefreshContext.ts";
import { useRefreshContext } from "./hooks/RefreshContext.ts";
const Organisations: React.FC = () => {
const auth = useAuth();
@ -12,37 +12,39 @@ const Organisations: React.FC = () => {
React.useEffect(() => {
const fetchOrgData = async () => {
console.log("Fetching org data");
if (!auth.user) return
if (!auth.user) return;
try {
const requestOptions = {
method: 'GET',
method: "GET",
headers: {
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 orgsObject = await response.json();
setCurrentOrgData(orgsObject['organisations'])
setCurrentOrgData(orgsObject["organisations"]);
} catch (e) {
console.error(e);
}
}
fetchOrgData()
.catch(console.error);
};
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} />);
panels = orgData.map((org) => (
<OrgPanel {...org} key={org.organisation_id} />
));
}
return(<>
return (
<>
<h1>Your organisations</h1>
{panels}
</>);
}
</>
);
};
export default Organisations;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,27 +1,29 @@
import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import {AuthProvider, type AuthProviderProps} from "react-oidc-context";
import {type User, WebStorageStateStore} from "oidc-client-ts";
import {BrowserRouter} from "react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { AuthProvider, type AuthProviderProps } from "react-oidc-context";
import { type User, WebStorageStateStore } from "oidc-client-ts";
import { BrowserRouter } from "react-router";
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}),
userStore: new WebStorageStateStore({ store: window.localStorage }),
};
createRoot(document.getElementById('root')!).render(<StrictMode>
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AuthProvider {...oidcConfig}>
<BrowserRouter basename="/ui">
<App/>
<App />
</BrowserRouter>
</AuthProvider>
</StrictMode>,)
</StrictMode>,
);

View file

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