mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06cb0ec3d | ||
|
|
0c924f7ded | ||
|
|
f29c915fc2 | ||
|
|
5f8579bfda | ||
|
|
a71389aa29 | ||
|
|
c3ba026452 | ||
|
|
193f8a7bdf | ||
|
|
f9814e1169 | ||
|
|
2f800bf912 | ||
|
|
0199ea81f3 | ||
|
|
a20894092b |
No files matched your search
@@ -0,0 +1,15 @@
|
||||
name: Codespell
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
only_warn: 1
|
||||
skip: package-lock.json,*.svg
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
# Wiretrustee Dashboard
|
||||
Wiretrustee Dashboard is a the Wiretrustee Managemenet server UI. It allow users to signin, view setup keys and manage peers. This image is **not ready** for production use.
|
||||
Wiretrustee Dashboard is a the Wiretrustee Management server UI. It allow users to signin, view setup keys and manage peers. This image is **not ready** for production use.
|
||||
## Tags
|
||||
```latest``` ```vX.X.X``` not available yet.
|
||||
|
||||
```main``` builded on every PR being merged to the repository
|
||||
```main``` built on every PR being merged to the repository
|
||||
## How to use this image
|
||||
HTTP run:
|
||||
```shell
|
||||
docker run -d --rm -p 80:80 wiretrustee/dashboard:main
|
||||
```
|
||||
Using SSL certificate from Let's Encript®:
|
||||
Using SSL certificate from Let's Encrypt®:
|
||||
```shell
|
||||
docker run -d --rm -p 80:80 -p 443:443 \
|
||||
-e LETSENCRYPT_DOMAIN=app.mydomain.com \
|
||||
|
||||
@@ -387,7 +387,7 @@ check_nb_domain() {
|
||||
|
||||
if [ "$DOMAIN" == "netbird.example.com" ]; then
|
||||
echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
|
||||
retrun 1
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class AccessControlPage {
|
||||
}
|
||||
|
||||
async assertDefaultAccessCotrolIsCreated() {
|
||||
await test.step('Assert that default cotrol access is created', async () => {
|
||||
await test.step('Assert that default control access is created', async () => {
|
||||
await expect(this.defaulAccessControl).toBeVisible();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ const NameServerGroupAdd = () => {
|
||||
};
|
||||
|
||||
const onChange = (changedValues: any) => {
|
||||
|
||||
if (changedValues.primary !== undefined) {
|
||||
setIsPrimary(changedValues.primary);
|
||||
}
|
||||
@@ -207,6 +208,7 @@ const NameServerGroupAdd = () => {
|
||||
let nsGroupLocal = {} as NameServerGroup;
|
||||
if (value === customChoice) {
|
||||
nsGroupLocal = nsGroup;
|
||||
nsGroupLocal.domains = [];
|
||||
} else {
|
||||
defaultDNSOptions.forEach((nsg) => {
|
||||
if (value === nsg.name) {
|
||||
|
||||
@@ -161,7 +161,7 @@ const Navbar = () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.role?.toLowerCase() === "admin") {
|
||||
if (user.role?.toLowerCase() === "admin" || user.role?.toLowerCase() === "owner") {
|
||||
return true;
|
||||
}
|
||||
return !adminOnlyTabs.find((t) => t === key);
|
||||
|
||||
+23
-10
@@ -79,6 +79,7 @@ const UserEdit = (props: any) => {
|
||||
const [formUser, setFormUser] = useState({} as FormUser);
|
||||
const [form] = Form.useForm();
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [isOwner, setIsOwner] = useState(false);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
@@ -94,15 +95,21 @@ const UserEdit = (props: any) => {
|
||||
};
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups =
|
||||
groups
|
||||
?.filter((g) => values.autoGroupsNames.includes(g.id))
|
||||
.map((g) => g.id || "") || [];
|
||||
let autoGroups:string[] = []
|
||||
if (values.autoGroupsNames) {
|
||||
autoGroups =
|
||||
groups
|
||||
?.filter((g) => values.autoGroupsNames.includes(g.id))
|
||||
.map((g) => g.id || "") || [];
|
||||
}
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map((g) => g.id || "");
|
||||
const groupsToCreate = values.autoGroupsNames.filter(
|
||||
(s: string) => !allGroupsNames.includes(s)
|
||||
);
|
||||
let groupsToCreate:string[] = []
|
||||
if (values.autoGroupsNames) {
|
||||
groupsToCreate = values.autoGroupsNames.filter(
|
||||
(s: string) => !allGroupsNames.includes(s)
|
||||
);
|
||||
}
|
||||
let userID = user ? user.id : "";
|
||||
let isServiceUser = user ? user?.is_service_user : false;
|
||||
return {
|
||||
@@ -120,7 +127,8 @@ const UserEdit = (props: any) => {
|
||||
if (users) {
|
||||
let currentUser = users.find((user) => user?.is_current);
|
||||
if (currentUser) {
|
||||
setIsAdmin(currentUser.role === "admin");
|
||||
setIsAdmin(currentUser.role === "admin" || currentUser.role === "owner");
|
||||
setIsOwner(currentUser.role === "owner");
|
||||
}
|
||||
}
|
||||
}, [users]);
|
||||
@@ -403,7 +411,7 @@ const UserEdit = (props: any) => {
|
||||
>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
disabled={user?.is_current}
|
||||
disabled={user?.is_current || user?.role === "owner"}
|
||||
>
|
||||
<Option value="admin">
|
||||
<Text type={"secondary"}>admin</Text>
|
||||
@@ -411,6 +419,11 @@ const UserEdit = (props: any) => {
|
||||
<Option value="user">
|
||||
<Text type={"secondary"}>user</Text>
|
||||
</Option>
|
||||
{!user?.is_service_user && isOwner && (
|
||||
<Option value="owner">
|
||||
<Text type={"secondary"}>owner</Text>
|
||||
</Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -473,7 +486,7 @@ const UserEdit = (props: any) => {
|
||||
label="Block user"
|
||||
style={{ marginRight: "50px", fontWeight: "500" }}
|
||||
>
|
||||
<Switch />
|
||||
<Switch disabled={user?.role == "owner"} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
|
||||
import {Account} from './types';
|
||||
import {ApiError, ChangeResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import {ApiError, ChangeResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getAccounts: createAsyncAction(
|
||||
@@ -16,6 +16,13 @@ const actions = {
|
||||
)<RequestPayload<Account>, ChangeResponse<Account | null>, ChangeResponse<Account | null>>(),
|
||||
setUpdateAccount: createAction('SET_UPDATED_ACCOUNT')<ChangeResponse<Account | null>>(),
|
||||
resetUpdateAccount: createAction('RESET_UPDATED_ACCOUNT')<null>(),
|
||||
deleteAccount: createAsyncAction(
|
||||
'DELETE_ACCOUNT_REQUEST',
|
||||
'DELETE_ACCOUNT_SUCCESS',
|
||||
'DELETE_ACCOUNT_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
resetDeletedAccount: createAction('RESET_DELETED_ACCOUNT')<null>(),
|
||||
setDeleteAccount: createAction('SET_DELETE_ACCOUNT')<DeleteResponse<string | null>>(),
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@ import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Account } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, ChangeResponse} from "../../services/api-client/types";
|
||||
import {ApiError, ChangeResponse, DeleteResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: Account[] | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
savedAccount: ChangeResponse<Account | null>;
|
||||
deletedAccount: DeleteResponse<string | null>;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
@@ -22,6 +23,13 @@ const initialState: StateType = {
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
deletedAccount: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
};
|
||||
|
||||
const data = createReducer<Account[], ActionTypes>(initialState.data as Account[])
|
||||
@@ -45,10 +53,18 @@ const updatedAccount = createReducer<ChangeResponse<Account | null>, ActionTypes
|
||||
.handleAction(actions.setUpdateAccount, (store, action) => action.payload)
|
||||
.handleAction(actions.resetUpdateAccount, () => initialState.savedAccount)
|
||||
|
||||
const deleteAccount = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedAccount)
|
||||
.handleAction(actions.deleteAccount.request, () => initialState.deletedAccount)
|
||||
.handleAction(actions.deleteAccount.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deleteAccount.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeleteAccount, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedAccount, () => initialState.deletedAccount)
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
loading,
|
||||
failed,
|
||||
updatedAccount
|
||||
updatedAccount,
|
||||
deleteAccount
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import {all, call, put, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, ChangeResponse} from '../../services/api-client/types';
|
||||
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, ChangeResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {Account} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import {deletePeer} from "../peer/sagas";
|
||||
|
||||
export function* getAccounts(action: ReturnType<typeof actions.getAccounts.request>): Generator {
|
||||
try {
|
||||
@@ -55,10 +56,55 @@ export function* updateAccount(action: ReturnType<typeof actions.updateAccount.r
|
||||
}
|
||||
}
|
||||
|
||||
export function* deleteAccount(
|
||||
action: ReturnType<typeof actions.deleteAccount.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield call(actions.setDeleteAccount, {
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>);
|
||||
|
||||
const effect = yield call(service.deleteAccount, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(
|
||||
actions.deleteAccount.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
|
||||
const accounts = (yield select((state) => state.peer.data)) as Account[];
|
||||
yield put(
|
||||
actions.getAccounts.success(
|
||||
accounts.filter((p: Account) => p.id !== action.payload.payload)
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
yield put(
|
||||
actions.deleteAccount.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getAccounts.request, getAccounts),
|
||||
takeLatest(actions.updateAccount.request, updateAccount),
|
||||
takeLatest(actions.deleteAccount.request, deleteAccount),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -15,5 +15,11 @@ export default {
|
||||
`/api/accounts/${id}`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
},
|
||||
async deleteAccount(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/accounts/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -8,6 +8,9 @@ export interface Account {
|
||||
jwt_groups_enabled: boolean;
|
||||
groups_propagation_enabled: boolean;
|
||||
jwt_groups_claim_name: string;
|
||||
extra: {
|
||||
peer_approval_enabled: boolean;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,4 +20,5 @@ export interface FormAccount extends Account {
|
||||
groups_propagation_enabled: boolean;
|
||||
jwt_groups_claim_name: string;
|
||||
peer_login_expiration_formatted: ExpiresInValue;
|
||||
peer_approval_enabled: boolean;
|
||||
}
|
||||
@@ -16,7 +16,8 @@ export interface Peer {
|
||||
dns_label: string,
|
||||
last_login: string,
|
||||
login_expired: boolean,
|
||||
login_expiration_enabled: boolean
|
||||
login_expiration_enabled: boolean,
|
||||
approval_required: boolean
|
||||
}
|
||||
|
||||
export interface FormPeer extends Peer {
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ export const transformGroupedDataTable = (
|
||||
}
|
||||
});
|
||||
groupList = groupList.filter(
|
||||
(value, index, arrary) => arrary.indexOf(value) === index
|
||||
(value, index, array) => array.indexOf(value) === index
|
||||
);
|
||||
let groupDataTableRoutes = transformDataTable(listedRoutes, peers);
|
||||
const filterEnabledRoutes = groupDataTableRoutes.filter(
|
||||
|
||||
@@ -338,6 +338,8 @@ export const Activity = () => {
|
||||
case "peer.login.expiration.enable":
|
||||
case "user.peer.login":
|
||||
case "peer.login.expire":
|
||||
case "peer.approve":
|
||||
case "peer.approval.revoke":
|
||||
return renderMultiRowSpan(event.meta.fqdn, event.meta.ip);
|
||||
case "route.add":
|
||||
case "route.delete":
|
||||
@@ -362,6 +364,20 @@ export const Activity = () => {
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
case "transferred.owner.role":
|
||||
if (event.meta.email || event.meta.username || event.target_id) {
|
||||
return renderMultiRowSpan(
|
||||
event.meta.username ? event.meta.username : event.target_id,
|
||||
event.meta.email ? event.meta.email : "User"
|
||||
);
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(
|
||||
event.meta.user_name,
|
||||
event.meta.is_service_user ? "Service User" : "User"
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
case "setupkey.group.add":
|
||||
case "setupkey.group.delete":
|
||||
return renderMultiRowSpan(event.meta.setupkey, "Setup Key");
|
||||
@@ -373,6 +389,8 @@ export const Activity = () => {
|
||||
case "account.setting.peer.login.expiration.enable":
|
||||
case "account.setting.peer.login.expiration.disable":
|
||||
case "account.setting.peer.login.expiration.update":
|
||||
case "account.setting.peer.approval.enable":
|
||||
case "account.setting.peer.approval.disable":
|
||||
case "integration.create":
|
||||
case "integration.update":
|
||||
case "integration.delete":
|
||||
|
||||
+68
-6
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { capitalize, formatOS, timeAgo } from "../utils/common";
|
||||
import {capitalize, formatOS, isLocalDev, isNetBirdHosted, timeAgo} from "../utils/common";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as peerActions } from "../store/peer";
|
||||
@@ -79,6 +79,7 @@ export const Peers = () => {
|
||||
(state: RootState) => state.peer.updateGroupsVisible
|
||||
);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const account = useSelector((state: RootState) => state.account.data);
|
||||
const [addPeerModalOpen, setAddPeerModalOpen] = useState(false);
|
||||
const { oidcUser } = useOidcUser();
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
@@ -100,6 +101,7 @@ export const Peers = () => {
|
||||
const optionsOnOff = [
|
||||
{ label: "Online", value: "on" },
|
||||
{ label: "All", value: "all" },
|
||||
// ...((isNetBirdHosted() || isLocalDev()) && account[0].settings.extra.peer_approval_enabled ? [{ label: "Needs approval", value: "approval" }] : []),
|
||||
];
|
||||
|
||||
const transformDataTable = (d: Peer[]): PeerDataTable[] => {
|
||||
@@ -126,7 +128,7 @@ export const Peers = () => {
|
||||
if (users) {
|
||||
let currentUser = users.find((user) => user.is_current);
|
||||
if (currentUser) {
|
||||
setIsAdmin(currentUser.role === "admin");
|
||||
setIsAdmin(currentUser.role === "admin" || currentUser.role === "owner");
|
||||
}
|
||||
}
|
||||
}, [users]);
|
||||
@@ -291,6 +293,16 @@ export const Peers = () => {
|
||||
);
|
||||
}) as Peer[];
|
||||
|
||||
// switch (optionOnOff) {
|
||||
// case "on":
|
||||
// f = filter(f, (f: Peer) => f.connected);
|
||||
// break;
|
||||
// case "approval":
|
||||
// f = filter(f, (f: Peer) => f.approval_required);
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
if (optionOnOff === "on") {
|
||||
f = filter(f, (f: Peer) => f.connected);
|
||||
}
|
||||
@@ -410,6 +422,36 @@ export const Peers = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const showConfirmApprove = (record: PeerDataTable) => {
|
||||
setPeerToAction(record);
|
||||
|
||||
let content = (
|
||||
<Paragraph>
|
||||
Are you sure you want to approve this peer?
|
||||
</Paragraph>
|
||||
);
|
||||
|
||||
let name = record ? record.name : "";
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: <span className="font-500">Approve peer {name}</span>,
|
||||
width: 600,
|
||||
content: content,
|
||||
onOk() {
|
||||
record.approval_required = false
|
||||
dispatch(
|
||||
peerActions.updatePeer.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: record,
|
||||
})
|
||||
);
|
||||
},
|
||||
onCancel() {
|
||||
setPeerToAction(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const showConfirmEnableSSH = (record: PeerDataTable) => {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
@@ -601,6 +643,19 @@ export const Peers = () => {
|
||||
""
|
||||
);
|
||||
|
||||
let approval = peer.approval_required ? (
|
||||
<Tooltip title="The peer needs to be approved by an administrator before it can connect to other peers">
|
||||
<Tag color="gold">
|
||||
<Text
|
||||
style={{ fontSize: "10px", color: "rgba(212, 136, 6, 1)" }}
|
||||
type={"secondary"}
|
||||
>needs approval</Text>
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
const userEmail = users?.find((u) => u.id === peer.user_id)?.email;
|
||||
let expiry = !peer.login_expiration_enabled ? (
|
||||
<Tag>
|
||||
@@ -622,9 +677,9 @@ export const Peers = () => {
|
||||
<Row>
|
||||
<Text className="font-500"> {status}</Text>
|
||||
</Row>
|
||||
<Row>{loginExpire}</Row>
|
||||
</span>
|
||||
</Button>
|
||||
<Row>{loginExpire}</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -643,11 +698,18 @@ export const Peers = () => {
|
||||
<Row>
|
||||
<Text type="secondary">{userEmail}</Text>
|
||||
</Row>
|
||||
<Row style={{ minWidth: "195px" }}>
|
||||
{expiry} {loginExpire}
|
||||
</Row>
|
||||
</span>
|
||||
</Button>
|
||||
<Row style={{ minWidth: "195px", paddingLeft: "15px" }}>
|
||||
{expiry} {loginExpire} <Button
|
||||
type="text"
|
||||
style={{ height: "auto", whiteSpace: "normal", textAlign: "center", padding: "0px", margin: "0px" }}
|
||||
onClick={() => showConfirmApprove(peer)}
|
||||
className={!isAdmin ? "nohover" : ""}
|
||||
>
|
||||
{approval}
|
||||
</Button>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -262,7 +262,7 @@ export const RegularUsers = () => {
|
||||
if (users) {
|
||||
let currentUser = users.find((user) => user.is_current);
|
||||
if (currentUser) {
|
||||
setIsAdmin(currentUser.role === "admin");
|
||||
setIsAdmin(currentUser.role === "admin" || currentUser.role === "owner");
|
||||
}
|
||||
}
|
||||
}, [users]);
|
||||
@@ -758,16 +758,20 @@ export const RegularUsers = () => {
|
||||
<Switch
|
||||
size={"small"}
|
||||
checked={e}
|
||||
disabled={record.is_current}
|
||||
disabled={record.is_current || record.role === "owner"}
|
||||
onClick={(active: boolean) => {
|
||||
handleBlockUser(active, record);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (record.is_current) {
|
||||
if (record.is_current || record.role === "owner") {
|
||||
let title = "You can't block or unblock yourself"
|
||||
if(record.role === "owner") {
|
||||
title = "You can't block or unblock owners"
|
||||
}
|
||||
return (
|
||||
<Tooltip title="You can't block or unblock yourself">
|
||||
<Tooltip title={title}>
|
||||
<Empty
|
||||
image={""}
|
||||
description={""}
|
||||
@@ -803,9 +807,13 @@ export const RegularUsers = () => {
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (record.is_current) {
|
||||
if (record.is_current || record.role === "owner") {
|
||||
let title = "You can't delete yourself"
|
||||
if(record.role === "owner") {
|
||||
title = "You can't delete owners"
|
||||
}
|
||||
return (
|
||||
<Tooltip title="You can't delete yourself">
|
||||
<Tooltip title={title}>
|
||||
<Empty
|
||||
image={""}
|
||||
description={""}
|
||||
|
||||
+632
-234
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -52,7 +52,7 @@ export const Users = () => {
|
||||
if(users) {
|
||||
let currentUser = users.find((user) => user.is_current)
|
||||
if(currentUser) {
|
||||
setIsAdmin(currentUser.role === 'admin');
|
||||
setIsAdmin(currentUser.role === 'admin' || currentUser.role === 'owner');
|
||||
}
|
||||
}
|
||||
}, [users])
|
||||
|
||||
Reference in new issue
Block a user