mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baf37c2eaf | ||
|
|
01bcc620a5 | ||
|
|
ace2bb61ef | ||
|
|
f389862931 | ||
|
|
521df658ad | ||
|
|
4db17c119a | ||
|
|
230a4cb05e | ||
|
|
06316239de | ||
|
|
59eff85339 | ||
|
|
ffabdf8a1a | ||
|
|
0fe5aa13b1 | ||
|
|
7166eb6e2f | ||
|
|
c9f1955d6a | ||
|
|
c3236d05a1 | ||
|
|
25e8a52465 | ||
|
|
2b26198741 | ||
|
|
2b359b2140 | ||
|
|
d71d8214e7 | ||
|
|
321e4d8311 | ||
|
|
224a889de3 | ||
|
|
b59865ae05 |
No files matched your search
@@ -5,6 +5,7 @@ on:
|
||||
- main
|
||||
tags:
|
||||
- "**"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build_n_push:
|
||||
@@ -21,6 +22,9 @@ jobs:
|
||||
- name: Install dependecies
|
||||
run: npm install
|
||||
|
||||
- name: List dependencies
|
||||
run: npm list
|
||||
|
||||
- name: Build
|
||||
# skiping fail on warning for now
|
||||
run: CI=false npm run build
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/node_modules.bkp
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
@@ -26,3 +27,5 @@ src/auth_config.json
|
||||
.idea
|
||||
.eslintcache
|
||||
src/.local-config.json
|
||||
/public/OidcServiceWorker.js
|
||||
/public/OidcTrustedDomains.js
|
||||
+41
-19
@@ -1,19 +1,39 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if [[ -z "${AUTH0_DOMAIN}" ]]; then
|
||||
echo "AUTH0_DOMAIN environment variable must be set"
|
||||
exit 1
|
||||
if [[ -z "${AUTH_AUTHORITY}" ]]; then
|
||||
if [[ -z "${AUTH0_DOMAIN}" ]]; then
|
||||
echo "AUTH_AUTHORITY or AUTH0_DOMAIN environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${AUTH0_CLIENT_ID}" ]]; then
|
||||
echo "AUTH0_CLIENT_ID environment variable must be set"
|
||||
exit 1
|
||||
if [[ -z "${AUTH_CLIENT_ID}" ]]; then
|
||||
if [[ -z "${AUTH0_CLIENT_ID}" ]]; then
|
||||
echo "AUTH_CLIENT_ID or AUTH0_CLIENT_ID environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${AUTH0_AUDIENCE}" ]]; then
|
||||
echo "AUTH0_AUDIENCE environment variable must be set"
|
||||
exit 1
|
||||
if [[ -z "${AUTH_AUDIENCE}" ]]; then
|
||||
if [[ -z "${AUTH0_AUDIENCE}" ]]; then
|
||||
echo "AUTH_AUDIENCE or AUTH0_AUDIENCE environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${AUTH_SUPPORTED_SCOPES}" ]]; then
|
||||
if [[ -z "${AUTH0_DOMAIN}" ]]; then
|
||||
echo "AUTH_SUPPORTED_SCOPES environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${USE_AUTH0}" ]]; then
|
||||
if [[ -z "${AUTH0_DOMAIN}" ]]; then
|
||||
echo "USE_AUTH0 environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
|
||||
@@ -21,11 +41,14 @@ if [[ -z "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
AUTH0_DOMAIN=${AUTH0_DOMAIN}
|
||||
AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}
|
||||
AUTH0_AUDIENCE=${AUTH0_AUDIENCE}
|
||||
NETBIRD_MGMT_API_ENDPOINT=${NETBIRD_MGMT_API_ENDPOINT}
|
||||
NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
|
||||
export AUTH_AUTHORITY=${AUTH_AUTHORITY:-https://$AUTH0_DOMAIN}
|
||||
export AUTH_CLIENT_ID=${AUTH_CLIENT_ID:-$AUTH0_CLIENT_ID}
|
||||
export AUTH_AUDIENCE=${AUTH_AUDIENCE:-$AUTH0_AUDIENCE}
|
||||
export USE_AUTH0=${USE_AUTH0:-true}
|
||||
export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api offline_access email_verified}
|
||||
|
||||
export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
|
||||
|
||||
REPO="https://github.com/netbirdio/netbird/"
|
||||
# this command will fetch the latest release e.g. v0.6.3
|
||||
@@ -33,11 +56,10 @@ export NETBIRD_LATEST_VERSION=$(basename $(curl -fs -o/dev/null -w %{redirect_ur
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
# replace ENVs in the config
|
||||
ENV_STR="\$\$AUTH0_DOMAIN \$\$AUTH0_CLIENT_ID \$\$AUTH0_AUDIENCE \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_LATEST_VERSION"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_LATEST_VERSION"
|
||||
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
cp "$MAIN_JS" "$MAIN_JS".copy
|
||||
envsubst "$ENV_STR" < "$MAIN_JS".copy > "$MAIN_JS"
|
||||
rm "$MAIN_JS".copy
|
||||
|
||||
|
||||
|
||||
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
|
||||
rm "$MAIN_JS".copy
|
||||
Generated
+20952
-2342
File diff suppressed because it is too large.
Load diff
+10
-5
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@auth0/auth0-react": "^1.6.0",
|
||||
"@axa-fr/react-oidc": "^5.14.0",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
@@ -21,18 +21,21 @@
|
||||
"antd": "^4.20.6",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"axios": "^0.27.2",
|
||||
"cidr-regex": "^3.1.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"heroicons": "^1.0.6",
|
||||
"highlight.js": "^11.2.0",
|
||||
"history": "^5.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "^8.4.12",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^18.1.0",
|
||||
"rc-overflow": "^1.2.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-highlight": "^0.14.0",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-table": "^7.7.0",
|
||||
"redux": "^4.2.0",
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
@@ -44,6 +47,8 @@
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"copy": "copyfiles -f ./node_modules/@axa-fr/react-oidc/dist/OidcServiceWorker.js ./public",
|
||||
"copytrusted": "copyfiles -f ./public/local/OidcTrustedDomains.js ./public",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
@@ -68,6 +73,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-highlight": "^0.12.5"
|
||||
"@types/react-syntax-highlighter": "^15.5.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
// Add here trusted domains, access tokens will be send to
|
||||
const trustedDomains = {
|
||||
default:["$NETBIRD_MGMT_API_ENDPOINT"],
|
||||
auth0:[]
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
// Add here trusted domains, access tokens will be send to
|
||||
const trustedDomains = {
|
||||
default:["http://localhost:3001","http://127.0.0.1:3001", "http://0.0.0.0:33071"],
|
||||
auth0:[]
|
||||
};
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
MGMT_PORT=$1
|
||||
|
||||
npm run build
|
||||
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
|
||||
|
||||
docker rm -f netbird-dashboard
|
||||
docker run -d --name netbird-dashboard \
|
||||
-p 3000:80 -p 443:443 \
|
||||
-e AUTH_AUDIENCE=netbird-client \
|
||||
-e AUTH_AUTHORITY=http://localhost:8080/realms/netbird \
|
||||
-e AUTH_CLIENT_ID=netbird-client \
|
||||
-e USE_AUTH0=false \
|
||||
-e AUTH_SUPPORTED_SCOPES='openid profile email api offline_access' \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
netbird/dashboard-local:latest
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
MGMT_PORT=$1
|
||||
|
||||
npm run build
|
||||
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
|
||||
|
||||
docker rm -f netbird-dashboard
|
||||
docker run -d --name netbird-dashboard \
|
||||
-p 3000:80 -p 443:443 \
|
||||
-e AUTH0_AUDIENCE=http://localhost:3000/ \
|
||||
-e AUTH0_DOMAIN=netbird-localdev.eu.auth0.com \
|
||||
-e AUTH0_CLIENT_ID=kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
netbird/dashboard-local:latest
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
MGMT_PORT=$1
|
||||
|
||||
npm run build
|
||||
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
|
||||
|
||||
docker rm -f netbird-dashboard
|
||||
docker run -d --name netbird-dashboard \
|
||||
-p 3000:80 -p 443:443 \
|
||||
-e AUTH_AUDIENCE=http://localhost:3000/ \
|
||||
-e AUTH_AUTHORITY=https://netbird-localdev.eu.auth0.com \
|
||||
-e AUTH_CLIENT_ID=kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt \
|
||||
-e USE_AUTH0=true \
|
||||
-e AUTH_SUPPORTED_SCOPES='openid profile email api offline_access email_verified' \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
netbird/dashboard-local:latest
|
||||
+10
-56
@@ -1,40 +1,26 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Provider} from "react-redux";
|
||||
import {Link, Redirect, Route, Switch} from 'react-router-dom';
|
||||
import {useAuth0} from "@auth0/auth0-react";
|
||||
import {Redirect, Route, Switch} from 'react-router-dom';
|
||||
import Navbar from './components/Navbar';
|
||||
import Peers from './views/Peers';
|
||||
import FooterComponent from './components/FooterComponent';
|
||||
import Loading from "./components/Loading";
|
||||
import SetupKeys from "./views/SetupKeys";
|
||||
import AddPeer from "./views/AddPeer";
|
||||
import Users from './views/Users';
|
||||
import AccessControl from './views/AccessControl';
|
||||
// import Activity from './views/Activity';
|
||||
import Routes from './views/Routes';
|
||||
import Banner from "./components/Banner";
|
||||
import {store} from "./store";
|
||||
|
||||
import {Button, Col, Layout, Result, Row} from 'antd';
|
||||
import { Col, Layout, Row} from 'antd';
|
||||
import {Container} from "./components/Container";
|
||||
import {withOidcSecure} from '@axa-fr/react-oidc';
|
||||
|
||||
const {Header, Content} = Layout;
|
||||
|
||||
function App() {
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
isAuthenticated,
|
||||
loginWithRedirect,
|
||||
logout,
|
||||
error
|
||||
} = useAuth0();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggle = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const hideMenu = () => {
|
||||
if (window.innerWidth > 768 && isOpen) {
|
||||
@@ -50,39 +36,8 @@ function App() {
|
||||
};
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <Result
|
||||
status="warning"
|
||||
title={error.message}
|
||||
extra={<>
|
||||
<a href={window.location.origin}>
|
||||
<Button type="primary">
|
||||
Try again
|
||||
</Button>
|
||||
</a>
|
||||
<Button type="primary" onClick={function () {
|
||||
logout({
|
||||
returnTo: window.location.origin,
|
||||
})
|
||||
}}>
|
||||
Log out
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading padding="3em" width="50px" height="50px"/>;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
loginWithRedirect({})
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
{isAuthenticated &&
|
||||
<Layout>
|
||||
<Banner/>
|
||||
<Header className="header" style={{
|
||||
@@ -110,17 +65,16 @@ function App() {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/peers' exact component={Peers}/>
|
||||
<Route path="/add-peer" component={AddPeer}/>
|
||||
<Route path="/setup-keys" component={SetupKeys}/>
|
||||
<Route path="/acls" component={AccessControl}/>
|
||||
{/*<Route path="/activity" component={Activity}/>*/}
|
||||
<Route path="/users" component={Users}/>
|
||||
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
|
||||
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
|
||||
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
}
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import { actions as ruleActions } from '../store/rule';
|
||||
import {string} from "prop-types";
|
||||
import {Avatar, List, Modal} from "antd";
|
||||
import {Group} from "../store/group/types";
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
import {ArrowRightOutlined, CheckOutlined, CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
|
||||
import {Rule, RuleToSave} from "../store/rule/types";
|
||||
import {useAuth0} from "@auth0/auth0-react";
|
||||
import { uniq } from "lodash"
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {useOidcAccessToken} from "@axa-fr/react-oidc";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
@@ -28,7 +28,7 @@ interface FormRule extends Rule {
|
||||
}
|
||||
|
||||
const AccessControlNew = () => {
|
||||
const { getAccessTokenSilently } = useAuth0()
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
@@ -96,7 +96,7 @@ const AccessControlNew = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const ruleToSave = createRuleToSave()
|
||||
dispatch(ruleActions.saveRule.request({getAccessTokenSilently, payload: ruleToSave}))
|
||||
dispatch(ruleActions.saveRule.request({getAccessTokenSilently:accessToken, payload: ruleToSave}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
@@ -308,14 +308,7 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="disabled"
|
||||
label="Status"
|
||||
//valuePropName="checked"
|
||||
>
|
||||
{/*<Switch
|
||||
checkedChildren={<CheckOutlined />}
|
||||
unCheckedChildren={<CloseOutlined />}
|
||||
|
||||
onChange={handleChangeDisabled}
|
||||
/>*/}
|
||||
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
@@ -330,7 +323,6 @@ const AccessControlNew = () => {
|
||||
name="tagSourceGroups"
|
||||
label="Source groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
@@ -352,7 +344,6 @@ const AccessControlNew = () => {
|
||||
name="tagDestinationGroups"
|
||||
label="Destination groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Select
|
||||
mode="tags" style={{ width: '100%' }}
|
||||
|
||||
@@ -14,7 +14,7 @@ const Banner = () => {
|
||||
const linkLearnMore = () => {
|
||||
return (
|
||||
<a
|
||||
href="https://netbird.io/blog/introducing-access-control"
|
||||
href="https://netbird.io/docs/how-to-guides/network-routes"
|
||||
className="font-bold underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -27,7 +27,7 @@ const Banner = () => {
|
||||
<Row>
|
||||
<Col xs={24} sm={0} lg={0}>
|
||||
<Text className="ant-col-md-0" style={{color: "#ffffff"}}>
|
||||
Big news! Introducing NetBird Access Control.
|
||||
New Release! Access private networks with the Network Routes feature.
|
||||
</Text>
|
||||
</Col>
|
||||
<Col xs={24} sm={0} lg={0}>
|
||||
@@ -38,7 +38,7 @@ const Banner = () => {
|
||||
<Col xs={0} sm={24}>
|
||||
<Space align="center" style={{display: "flex", justifyContent: "center"}}>
|
||||
<Text style={{color: "#ffffff"}}>
|
||||
Big news! Introducing NetBird Access Control.
|
||||
New Release! Access private networks with the Network Routes feature.
|
||||
</Text>
|
||||
<span>
|
||||
{linkLearnMore()}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import {OidcUserStatus, useOidc, useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {Button, Result} from "antd";
|
||||
import React from "react";
|
||||
import {getConfig} from "../config";
|
||||
|
||||
function LoginError() {
|
||||
const { logout } = useOidc();
|
||||
const config = getConfig();
|
||||
const { oidcUserLoadingState } = useOidcUser();
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
if (urlParams.get("error") === "access_denied") {
|
||||
return <Result
|
||||
status="warning"
|
||||
title={urlParams.get("error_description")}
|
||||
extra={<>
|
||||
<a href={window.location.origin}>
|
||||
<Button type="primary">
|
||||
Try again
|
||||
</Button>
|
||||
</a>
|
||||
<Button type="primary" onClick={function () {
|
||||
logout("",{client_id:config.clientId})
|
||||
}}>
|
||||
Log out
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
return <div>{"Login Error: User state: "+oidcUserLoadingState}</div>
|
||||
}
|
||||
|
||||
export default LoginError;
|
||||
+11
-15
@@ -1,23 +1,26 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import logo from "../assets/logo.png";
|
||||
import {useAuth0} from "@auth0/auth0-react";
|
||||
import {useLocation} from 'react-router-dom';
|
||||
import {Menu, Row, Col, Grid, Dropdown, Avatar, Button, Typography, Space} from 'antd'
|
||||
import {ItemType} from "antd/lib/menu/hooks/useItems";
|
||||
import {AvatarSize} from "antd/es/avatar/SizeContext";
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
|
||||
import { useOidc,useOidcUser } from '@axa-fr/react-oidc';
|
||||
import {getConfig} from "../config";
|
||||
const { Text } = Typography
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const Navbar = () => {
|
||||
let location = useLocation();
|
||||
const config = getConfig();
|
||||
const {
|
||||
user,
|
||||
isAuthenticated,
|
||||
logout,
|
||||
} = useAuth0();
|
||||
} = useOidc();
|
||||
|
||||
const { oidcUser } = useOidcUser();
|
||||
const user = oidcUser;
|
||||
|
||||
const screens = useBreakpoint();
|
||||
|
||||
@@ -31,10 +34,11 @@ const Navbar = () => {
|
||||
{ label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer' },
|
||||
{ label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys' },
|
||||
{ label: (<Link to="/acls">Access Control</Link>), key: '/acls' },
|
||||
// { label: (<Link to="/activity">Activity</Link>), key: '/activity' },
|
||||
{ label: (<Link to="/routes">Network Routes</Link>), key: '/routes' },
|
||||
{ label: (<Link to="/users">Users</Link>), key: '/users' }
|
||||
] as ItemType[])
|
||||
|
||||
const logoutWithRedirect = () =>
|
||||
logout("/",{client_id:config.clientId});
|
||||
useEffect(() => {
|
||||
const fs = menuItems.filter(m => m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
|
||||
if (screens.xs === true) {
|
||||
@@ -63,21 +67,13 @@ const Navbar = () => {
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
label: <a onClick={e => {
|
||||
logoutWithRedirect()
|
||||
e.preventDefault()}
|
||||
}>Logout</a>,
|
||||
label: (<Link to="/logout" onClick={logoutWithRedirect}>Logout</Link>),
|
||||
key: '1',
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const logoutWithRedirect = () =>
|
||||
logout({
|
||||
returnTo: window.location.origin,
|
||||
});
|
||||
|
||||
const createAvatar = (size:AvatarSize) => {
|
||||
return user?.picture ? (
|
||||
<Avatar size={size} src={user?.picture} icon={<UserOutlined />} />
|
||||
|
||||
@@ -5,17 +5,16 @@ import {actions as peerActions} from '../store/peer';
|
||||
import {Button, Col, Divider, Drawer, Form, Input, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import type {CustomTagProps} from 'rc-select/lib/BaseSelect'
|
||||
import {useAuth0} from "@auth0/auth0-react";
|
||||
import {Peer, PeerGroupsToSave} from "../store/peer/types";
|
||||
import {Group, GroupPeer} from "../store/group/types";
|
||||
import {CloseOutlined, EditOutlined, FlagFilled} from "@ant-design/icons";
|
||||
import {RuleObject} from 'antd/lib/form';
|
||||
|
||||
import { useOidcAccessToken } from '@axa-fr/react-oidc';
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const PeerUpdate = () => {
|
||||
const { getAccessTokenSilently } = useAuth0()
|
||||
const { accessToken } = useOidcAccessToken()
|
||||
const dispatch = useDispatch()
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const peer = useSelector((state: RootState) => state.peer.peer)
|
||||
@@ -214,11 +213,11 @@ const PeerUpdate = () => {
|
||||
if (!noUpdateToName()) {
|
||||
const peerUpdate = createPeerToSave()
|
||||
setCallingPeerAPI(true)
|
||||
dispatch(peerActions.updatePeer.request({getAccessTokenSilently, payload: peerUpdate}))
|
||||
dispatch(peerActions.updatePeer.request({getAccessTokenSilently:accessToken, payload: peerUpdate}))
|
||||
}
|
||||
if (peerGroupsToSave.groupsToRemove.length || peerGroupsToSave.groupsToAdd.length || peerGroupsToSave.groupsNoId.length) {
|
||||
setCallingGroupAPI(true)
|
||||
dispatch(peerActions.saveGroups.request({getAccessTokenSilently, payload: peerGroupsToSave}))
|
||||
dispatch(peerActions.saveGroups.request({getAccessTokenSilently:accessToken, payload: peerGroupsToSave}))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
@@ -278,7 +277,7 @@ const PeerUpdate = () => {
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && peer.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
<button type="button" aria-label="Close" autoFocus={true} className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
@@ -296,20 +295,18 @@ const PeerUpdate = () => {
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Update Name"
|
||||
label="Name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Input
|
||||
placeholder={peer.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
// onChange={(e) => handleChangeName(e.)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
</Col>
|
||||
@@ -323,17 +320,15 @@ const PeerUpdate = () => {
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Select groups to associate with this peer"
|
||||
label="Select peer groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{display: 'flex'}}
|
||||
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Select groups..."
|
||||
tagRender={tagRender}
|
||||
// onDropdownVisibleChange={evaluateSubmit}
|
||||
// onSelect={evaluateSubmit}
|
||||
|
||||
dropdownRender={dropDownRender}
|
||||
onChange={handleChangeTags}>
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import { actions as routeActions } from '../store/route';
|
||||
import {
|
||||
Col,
|
||||
Row,
|
||||
Input,
|
||||
InputNumber,
|
||||
Space,
|
||||
Switch,
|
||||
SelectProps,
|
||||
Button, Drawer, Form, Divider, Select, Radio, Typography
|
||||
} from "antd";
|
||||
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {Route} from "../store/route/types";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {useOidcAccessToken} from "@axa-fr/react-oidc";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import {
|
||||
masqueradeDisabledMSG,
|
||||
peerToPeerIP,
|
||||
initPeerMaps,
|
||||
routePeerSeparator,
|
||||
transformGroupedDataTable
|
||||
} from '../utils/routes'
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface FormRoute extends Route {
|
||||
}
|
||||
|
||||
const RouteUpdate = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
|
||||
const setupNewRouteHA = useSelector((state: RootState) => state.route.setupNewRouteHA)
|
||||
const peers = useSelector((state: RootState) => state.peer.data)
|
||||
const route = useSelector((state: RootState) => state.route.route)
|
||||
const routes = useSelector((state: RootState) => state.route.data)
|
||||
const savedRoute = useSelector((state: RootState) => state.route.savedRoute)
|
||||
// const [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]);
|
||||
const [previousRouteKey, setPreviousRouteKey] = useState("")
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [editDescription, setEditDescription] = useState(false)
|
||||
const options: SelectProps['options'] = [];
|
||||
const [formRoute, setFormRoute] = useState({} as FormRoute)
|
||||
const [form] = Form.useForm()
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const inputDescriptionRef = useRef<any>(null)
|
||||
|
||||
const defaultRoutingPeerMSG = "Routing Peer"
|
||||
const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG)
|
||||
const defaultMasqueradeMSG = "Masquerade"
|
||||
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG)
|
||||
const defaultStatusMSG = "Status"
|
||||
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG)
|
||||
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
|
||||
|
||||
useEffect(() => {
|
||||
if (setupNewRouteHA) {
|
||||
setRoutingPeerMSG("Add additional routing peer")
|
||||
setMasqueradeMSG("Update Masquerade")
|
||||
setStatusMSG("Update Status")
|
||||
} else {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG)
|
||||
setMasqueradeMSG(defaultMasqueradeMSG)
|
||||
setStatusMSG(defaultStatusMSG)
|
||||
setPreviousRouteKey("")
|
||||
}
|
||||
}, [setupNewRouteHA])
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription) inputDescriptionRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!route) return
|
||||
|
||||
const fRoute = {
|
||||
...route,
|
||||
} as FormRoute
|
||||
setFormRoute(fRoute)
|
||||
setPreviousRouteKey(fRoute.network_id+fRoute.network)
|
||||
form.setFieldsValue(fRoute)
|
||||
}, [route])
|
||||
|
||||
peers.forEach((p) => {
|
||||
let os:string
|
||||
os = p.os
|
||||
if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows")) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(p.name,p.ip),
|
||||
value: peerToPeerIP(p.name,p.ip),
|
||||
disabled: false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const createRouteToSave = (inputRoute:FormRoute):Route => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator)
|
||||
let peerID:string
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIDList[1]
|
||||
} else {
|
||||
peerID = peerNameToIP[inputRoute.peer]
|
||||
}
|
||||
|
||||
return {
|
||||
id: inputRoute.id,
|
||||
network: inputRoute.network,
|
||||
network_id: inputRoute.network_id,
|
||||
description: inputRoute.description,
|
||||
peer: peerID,
|
||||
enabled: inputRoute.enabled,
|
||||
masquerade: inputRoute.masquerade,
|
||||
metric: inputRoute.metric
|
||||
} as Route
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then(() => {
|
||||
if (!setupNewRouteHA || formRoute.peer != '') {
|
||||
const routeToSave = createRouteToSave(formRoute)
|
||||
dispatch(routeActions.saveRoute.request({getAccessTokenSilently:accessToken, payload: routeToSave}))
|
||||
} else {
|
||||
let groupedDataTable = transformGroupedDataTable(routes,peerIPToName)
|
||||
groupedDataTable.forEach((group) => {
|
||||
if (group.key == previousRouteKey) {
|
||||
group.groupedRoutes.forEach((route) => {
|
||||
let updateRoute:FormRoute = {
|
||||
...formRoute,
|
||||
id: route.id,
|
||||
peer: route.peer,
|
||||
metric: route.metric,
|
||||
enabled: (formRoute.enabled != group.enabled) ? formRoute.enabled : route.enabled
|
||||
}
|
||||
const routeToSave = createRouteToSave(updateRoute)
|
||||
dispatch(routeActions.saveRoute.request({getAccessTokenSilently:accessToken, payload: routeToSave}))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRoute = (status:boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteVisible(status));
|
||||
}
|
||||
|
||||
const setSetupNewRouteHA = (status:boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(status));
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedRoute.loading) return
|
||||
setEditName(false)
|
||||
dispatch(routeActions.setRoute({
|
||||
network: '',
|
||||
network_id: '',
|
||||
description: '',
|
||||
peer: "",
|
||||
metric: 9999,
|
||||
masquerade: false,
|
||||
enabled: true
|
||||
} as Route))
|
||||
setVisibleNewRoute(false)
|
||||
setSetupNewRouteHA(false)
|
||||
setPreviousRouteKey("")
|
||||
}
|
||||
|
||||
const onChange = (data:any) => {
|
||||
setFormRoute({...formRoute, ...data})
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
</>
|
||||
)
|
||||
|
||||
const toggleEditName = (status:boolean) => {
|
||||
setEditName(status);
|
||||
}
|
||||
|
||||
const toggleEditDescription = (status:boolean) => {
|
||||
setEditDescription(status);
|
||||
}
|
||||
|
||||
const networkRangeValidator = (_: RuleObject, value: string) => {
|
||||
if (!cidrRegex().test(value)) {
|
||||
return Promise.reject(new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24"))
|
||||
}
|
||||
|
||||
if (Number(value.split("/")[1]) < 7) {
|
||||
return Promise.reject(new Error("Please enter a network mask larger than /7"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{route &&
|
||||
<Drawer
|
||||
headerStyle={{display: "none"}}
|
||||
forceRender={true}
|
||||
visible={setupNewRouteVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedRoute.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedRoute.loading} onClick={handleFormSubmit}>{`${formRoute.network_id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && !editDescription && formRoute.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close" className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{ !editName && formRoute.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRoute.id ? formRoute.network_id : 'New Route'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="network_id"
|
||||
label="Network Identifier"
|
||||
tooltip="You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes"
|
||||
rules={[{required: true, message: 'Please add an identifier for this access route', whitespace: true}]}
|
||||
>
|
||||
<Input placeholder="e.g. aws-eu-central-1-vpc" ref={inputNameRef} disabled={!setupNewRouteHA} onPressEnter={() => toggleEditName(false)} onBlur={() => toggleEditName(false)} autoComplete="off" maxLength={40}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{ !editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"} onClick={() => toggleEditDescription(true)}>{formRoute.description && formRoute.description.trim() !== "" ? formRoute.description : 'Add description...'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef} disabled={!setupNewRouteHA} onPressEnter={() => toggleEditDescription(false)} onBlur={() => toggleEditDescription(false)} autoComplete="off" maxLength={200}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="network"
|
||||
label="Network Range"
|
||||
tooltip="Use CIDR notation. e.g. 192.168.10.0/24 or 172.16.0.0/16"
|
||||
rules={[{validator: networkRangeValidator}]}
|
||||
>
|
||||
<Input placeholder="e.g. 172.16.0.0/16" disabled={!setupNewRouteHA} autoComplete="off" minLength={9} maxLength={43}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label={statusMSG}
|
||||
>
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="peer"
|
||||
label={routingPeerMSG}
|
||||
tooltip="Assign a peer as a routing peer for the Network CIDR"
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={dropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="masquerade"
|
||||
label={masqueradeMSG}
|
||||
tooltip={masqueradeDisabledMSG}
|
||||
>
|
||||
<Switch size={"small"} disabled={!setupNewRouteHA} checked={formRoute.masquerade}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="metric"
|
||||
label="Metric"
|
||||
tooltip="Choose from 1 to 9999. Lower number has higher priority"
|
||||
>
|
||||
<InputNumber min={1} max={9999} autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
<FlagFilled/>
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/network-routes" style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about network routes</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RouteUpdate
|
||||
+376
-93
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,8 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import Highlight from 'react-highlight';
|
||||
import "highlight.js/styles/mono-blue.css";
|
||||
import "highlight.js/lib/languages/bash";
|
||||
import { StepCommand } from './types'
|
||||
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { monoBlue } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
import {
|
||||
Typography,
|
||||
Space,
|
||||
@@ -12,8 +11,6 @@ import {
|
||||
import {copyToClipboard} from "../../utils/common";
|
||||
import {CheckOutlined, CopyOutlined} from "@ant-design/icons";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Step } = Steps;
|
||||
|
||||
type Props = {
|
||||
@@ -48,10 +45,10 @@ const TabSteps:React.FC<Props> = ({stepsItems}) => {
|
||||
title={c.title}
|
||||
description={
|
||||
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex"}}>
|
||||
{ (c.commands && (typeof c.commands === 'string' || c.commands instanceof String)) ? (
|
||||
<Highlight className='bash'>
|
||||
{ (c.commands && (typeof c.commands === 'string')) ? (
|
||||
<SyntaxHighlighter language="bash" style={monoBlue}>
|
||||
{c.commands}
|
||||
</Highlight>
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
c.commands
|
||||
)}
|
||||
|
||||
Loaded 20 of 50 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user