Skip to content

Commit 43599b4

Browse files
committed
Access list modal polish
1 parent 227e818 commit 43599b4

File tree

18 files changed

+376
-73
lines changed

18 files changed

+376
-73
lines changed

frontend/src/App.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,24 @@
7474
label.row {
7575
cursor: pointer;
7676
}
77+
78+
.input-group-select {
79+
display: flex;
80+
align-items: center;
81+
padding: 0;
82+
font-size: .875rem;
83+
font-weight: 400;
84+
line-height: 1.25rem;
85+
color: var(--tblr-gray-500);
86+
text-align: center;
87+
white-space: nowrap;
88+
background-color: var(--tblr-bg-surface-secondary);
89+
border: var(--tblr-border-width) solid var(--tblr-border-color);
90+
border-radius: var(--tblr-border-radius);
91+
92+
.form-select {
93+
border: none;
94+
background-color: var(--tblr-bg-surface-secondary);
95+
border-radius: var(--tblr-border-radius) 0 0 var(--tblr-border-radius);
96+
}
97+
}

frontend/src/api/backend/models.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ export interface AccessListItem {
6767
accessListId?: number;
6868
username: string;
6969
password: string;
70-
meta: Record<string, any>;
71-
hint: string;
70+
meta?: Record<string, any>;
71+
hint?: string;
7272
}
7373

7474
export type AccessListClient = {
@@ -78,7 +78,7 @@ export type AccessListClient = {
7878
accessListId?: number;
7979
address: string;
8080
directive: "allow" | "deny";
81-
meta: Record<string, any>;
81+
meta?: Record<string, any>;
8282
};
8383

8484
export interface Certificate {
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { IconX } from "@tabler/icons-react";
2+
import cn from "classnames";
3+
import { useFormikContext } from "formik";
4+
import { useState } from "react";
5+
import type { AccessListClient } from "src/api/backend";
6+
import { T } from "src/locale";
7+
8+
interface Props {
9+
initialValues: AccessListClient[];
10+
name?: string;
11+
}
12+
export function AccessClientFields({ initialValues, name = "clients" }: Props) {
13+
const [values, setValues] = useState<AccessListClient[]>(initialValues || []);
14+
const { setFieldValue } = useFormikContext();
15+
16+
const blankClient: AccessListClient = { directive: "allow", address: "" };
17+
18+
if (values?.length === 0) {
19+
setValues([blankClient]);
20+
}
21+
22+
const handleAdd = () => {
23+
setValues([...values, blankClient]);
24+
};
25+
26+
const handleRemove = (idx: number) => {
27+
const newValues = values.filter((_: AccessListClient, i: number) => i !== idx);
28+
if (newValues.length === 0) {
29+
newValues.push(blankClient);
30+
}
31+
setValues(newValues);
32+
setFormField(newValues);
33+
};
34+
35+
const handleChange = (idx: number, field: string, fieldValue: string) => {
36+
const newValues = values.map((v: AccessListClient, i: number) =>
37+
i === idx ? { ...v, [field]: fieldValue } : v,
38+
);
39+
setValues(newValues);
40+
setFormField(newValues);
41+
};
42+
43+
const setFormField = (newValues: AccessListClient[]) => {
44+
const filtered = newValues.filter((v: AccessListClient) => v?.address?.trim() !== "");
45+
setFieldValue(name, filtered);
46+
};
47+
48+
return (
49+
<>
50+
<p className="text-muted">
51+
<T id="access.help.rules-order" />
52+
</p>
53+
{values.map((client: AccessListClient, idx: number) => (
54+
<div className="row mb-1" key={idx}>
55+
<div className="col-11">
56+
<div className="input-group mb-2">
57+
<span className="input-group-select">
58+
<select
59+
className={cn(
60+
"form-select",
61+
"m-0",
62+
client.directive === "allow" ? "bg-lime-lt" : "bg-orange-lt",
63+
)}
64+
name={`clients[${idx}].directive`}
65+
value={client.directive}
66+
onChange={(e) => handleChange(idx, "directive", e.target.value)}
67+
>
68+
<option value="allow">Allow</option>
69+
<option value="deny">Deny</option>
70+
</select>
71+
</span>
72+
<input
73+
name={`clients[${idx}].address`}
74+
type="text"
75+
className="form-control"
76+
autoComplete="off"
77+
value={client.address}
78+
onChange={(e) => handleChange(idx, "address", e.target.value)}
79+
placeholder="192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32"
80+
/>
81+
</div>
82+
</div>
83+
<div className="col-1">
84+
<a
85+
role="button"
86+
className="btn btn-ghost btn-danger p-0"
87+
onClick={(e) => {
88+
e.preventDefault();
89+
handleRemove(idx);
90+
}}
91+
>
92+
<IconX size={16} />
93+
</a>
94+
</div>
95+
</div>
96+
))}
97+
<div className="mb-3">
98+
<button type="button" className="btn btn-sm" onClick={handleAdd}>
99+
<T id="action.add" />
100+
</button>
101+
</div>
102+
<div className="row mb-3">
103+
<p className="text-muted">
104+
<T id="access.help-rules-last" />
105+
</p>
106+
<div className="col-11">
107+
<div className="input-group mb-2">
108+
<span className="input-group-select">
109+
<select
110+
className="form-select m-0 bg-orange-lt"
111+
name="clients[last].directive"
112+
value="deny"
113+
disabled
114+
>
115+
<option value="deny">Deny</option>
116+
</select>
117+
</span>
118+
<input
119+
name="clients[last].address"
120+
type="text"
121+
className="form-control"
122+
autoComplete="off"
123+
value="all"
124+
disabled
125+
/>
126+
</div>
127+
</div>
128+
</div>
129+
</>
130+
);
131+
}

frontend/src/components/Form/AccessField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ interface Props {
3232
label?: string;
3333
}
3434
export function AccessField({ name = "accessListId", label = "access.title", id = "accessListId" }: Props) {
35-
const { isLoading, isError, error, data } = useAccessLists();
35+
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
3636
const { setFieldValue } = useFormikContext();
3737

3838
const handleChange = (newValue: any, _actionMeta: ActionMeta<AccessOption>) => {

frontend/src/components/Form/BasicAuthField.tsx

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { IconX } from "@tabler/icons-react";
2+
import { useFormikContext } from "formik";
3+
import { useState } from "react";
4+
import type { AccessListItem } from "src/api/backend";
5+
import { T } from "src/locale";
6+
7+
interface Props {
8+
initialValues: AccessListItem[];
9+
name?: string;
10+
}
11+
export function BasicAuthFields({ initialValues, name = "items" }: Props) {
12+
const [values, setValues] = useState<AccessListItem[]>(initialValues || []);
13+
const { setFieldValue } = useFormikContext();
14+
15+
const blankItem: AccessListItem = { username: "", password: "" };
16+
17+
if (values?.length === 0) {
18+
setValues([blankItem]);
19+
}
20+
21+
const handleAdd = () => {
22+
setValues([...values, blankItem]);
23+
};
24+
25+
const handleRemove = (idx: number) => {
26+
const newValues = values.filter((_: AccessListItem, i: number) => i !== idx);
27+
if (newValues.length === 0) {
28+
newValues.push(blankItem);
29+
}
30+
setValues(newValues);
31+
setFormField(newValues);
32+
};
33+
34+
const handleChange = (idx: number, field: string, fieldValue: string) => {
35+
const newValues = values.map((v: AccessListItem, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));
36+
setValues(newValues);
37+
setFormField(newValues);
38+
};
39+
40+
const setFormField = (newValues: AccessListItem[]) => {
41+
const filtered = newValues.filter((v: AccessListItem) => v?.username?.trim() !== "");
42+
setFieldValue(name, filtered);
43+
};
44+
45+
return (
46+
<>
47+
<div className="row">
48+
<div className="col-6">
49+
<label className="form-label" htmlFor="...">
50+
<T id="username" />
51+
</label>
52+
</div>
53+
<div className="col-6">
54+
<label className="form-label" htmlFor="...">
55+
<T id="password" />
56+
</label>
57+
</div>
58+
</div>
59+
{values.map((item: AccessListItem, idx: number) => (
60+
<div className="row mb-3" key={idx}>
61+
<div className="col-6">
62+
<input
63+
type="text"
64+
autoComplete="off"
65+
className="form-control input-sm"
66+
value={item.username}
67+
onChange={(e) => handleChange(idx, "username", e.target.value)}
68+
/>
69+
</div>
70+
<div className="col-5">
71+
<input
72+
type="password"
73+
autoComplete="off"
74+
className="form-control"
75+
value={item.password}
76+
placeholder={
77+
initialValues.filter((iv: AccessListItem) => iv.username === item.username).length > 0
78+
? "••••••••"
79+
: ""
80+
}
81+
onChange={(e) => handleChange(idx, "password", e.target.value)}
82+
/>
83+
</div>
84+
<div className="col-1">
85+
<a
86+
role="button"
87+
className="btn btn-ghost btn-danger p-0"
88+
onClick={(e) => {
89+
e.preventDefault();
90+
handleRemove(idx);
91+
}}
92+
>
93+
<IconX size={16} />
94+
</a>
95+
</div>
96+
</div>
97+
))}
98+
<div>
99+
<button type="button" className="btn btn-sm" onClick={handleAdd}>
100+
<T id="action.add" />
101+
</button>
102+
</div>
103+
</>
104+
);
105+
}

frontend/src/components/Form/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
export * from "./AccessClientFields";
12
export * from "./AccessField";
2-
export * from "./BasicAuthField";
3+
export * from "./BasicAuthFields";
34
export * from "./DNSProviderFields";
45
export * from "./DomainNamesField";
56
export * from "./NginxConfigField";

frontend/src/hooks/useAccessList.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2-
import { type AccessList, createAccessList, getAccessList, updateAccessList } from "src/api/backend";
2+
import {
3+
type AccessList,
4+
type AccessListExpansion,
5+
createAccessList,
6+
getAccessList,
7+
updateAccessList,
8+
} from "src/api/backend";
39

4-
const fetchAccessList = (id: number | "new") => {
10+
const fetchAccessList = (id: number | "new", expand: AccessListExpansion[] = ["owner"]) => {
511
if (id === "new") {
612
return Promise.resolve({
713
id: 0,
@@ -14,13 +20,13 @@ const fetchAccessList = (id: number | "new") => {
1420
meta: {},
1521
} as AccessList);
1622
}
17-
return getAccessList(id, ["owner"]);
23+
return getAccessList(id, expand);
1824
};
1925

20-
const useAccessList = (id: number | "new", options = {}) => {
26+
const useAccessList = (id: number | "new", expand?: AccessListExpansion[], options = {}) => {
2127
return useQuery<AccessList, Error>({
22-
queryKey: ["access-list", id],
23-
queryFn: () => fetchAccessList(id),
28+
queryKey: ["access-list", id, expand],
29+
queryFn: () => fetchAccessList(id, expand),
2430
staleTime: 60 * 1000, // 1 minute
2531
...options,
2632
});
@@ -44,7 +50,7 @@ const useSetAccessList = () => {
4450
onError: (_, __, rollback: any) => rollback(),
4551
onSuccess: async ({ id }: AccessList) => {
4652
queryClient.invalidateQueries({ queryKey: ["access-list", id] });
47-
queryClient.invalidateQueries({ queryKey: ["access-list"] });
53+
queryClient.invalidateQueries({ queryKey: ["access-lists"] });
4854
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
4955
},
5056
});

frontend/src/hooks/useDeadHost.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const useSetDeadHost = () => {
5151
queryClient.invalidateQueries({ queryKey: ["dead-host", id] });
5252
queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
5353
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
54+
queryClient.invalidateQueries({ queryKey: ["host-report"] });
5455
},
5556
});
5657
};

frontend/src/hooks/useProxyHost.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const useSetProxyHost = () => {
5858
queryClient.invalidateQueries({ queryKey: ["proxy-host", id] });
5959
queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] });
6060
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
61+
queryClient.invalidateQueries({ queryKey: ["host-report"] });
6162
},
6263
});
6364
};

0 commit comments

Comments
 (0)