mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6845094723 | ||
|
|
b9f278360e | ||
|
|
6d3928d7c0 | ||
|
|
35fe6b6cbb | ||
|
|
dcfa2398e3 | ||
|
|
1cc2353957 | ||
|
|
7189a9913d | ||
|
|
6a37af081b | ||
|
|
4d21ce4680 | ||
|
|
9d8e4dc50d | ||
|
|
26edbb8add | ||
|
|
6791fb44a4 | ||
|
|
00a1fd8113 | ||
|
|
08bb65dfe8 | ||
|
|
a9a91905fa | ||
|
|
3459a4db28 | ||
|
|
cba2613e68 | ||
|
|
fe32179dc1 | ||
|
|
1bad86b277 | ||
|
|
f24775fe25 |
No files matched your search
File renamed without changes.
@@ -98,6 +98,24 @@ jobs:
|
||||
type=ref,event=pr
|
||||
type=sha
|
||||
|
||||
- name: Docker meta (rootless)
|
||||
id: meta_rootless
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_IMAGE }}
|
||||
${{ env.GHCR_IMAGE }}
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-rootless,onlatest=true
|
||||
tags: |
|
||||
type=schedule
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
type=ref,event=pr
|
||||
type=sha
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@v3
|
||||
@@ -123,7 +141,18 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Build and push rootless Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile.rootless
|
||||
push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
labels: ${{ steps.meta_rootless.outputs.labels }}
|
||||
|
||||
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
run: |
|
||||
echo '### Pushed tags' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ steps.meta.outputs.tags }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ steps.meta_rootless.outputs.tags }}' >> $GITHUB_STEP_SUMMARY
|
||||
@@ -11,6 +11,7 @@
|
||||
# ./build.sh # build + export netbird-dashboard.tar.gz
|
||||
# IMAGE_TAG=v1 ./build.sh # override the image tag
|
||||
# SKIP_BUILD=1 ./build.sh # reuse an existing out/ (skip npm build)
|
||||
# ROOTLESS=1 ./build.sh # build the amd64-rootless image variant
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
@@ -19,8 +20,26 @@ cd "$(dirname "$0")"
|
||||
|
||||
IMAGE_NAME="${IMAGE_NAME:-netbird-dashboard}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-amd64}"
|
||||
DOCKERFILE="docker/Dockerfile"
|
||||
CONTAINER_PORT=80
|
||||
OUTPUT_SUFFIX=""
|
||||
|
||||
case "${ROOTLESS:-0}" in
|
||||
0) ;;
|
||||
1)
|
||||
DOCKERFILE="docker/Dockerfile.rootless"
|
||||
CONTAINER_PORT=8080
|
||||
OUTPUT_SUFFIX="-rootless"
|
||||
[[ "${IMAGE_TAG}" == *-rootless ]] || IMAGE_TAG="${IMAGE_TAG}-rootless"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: ROOTLESS must be 0 or 1." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
OUTPUT="${OUTPUT:-${IMAGE_NAME}.tar.gz}"
|
||||
OUTPUT="${OUTPUT:-${IMAGE_NAME}${OUTPUT_SUFFIX}.tar.gz}"
|
||||
|
||||
# Build the static export (out/) unless told to reuse an existing one. Next 16
|
||||
# no longer runs ESLint during `next build`; types are checked by the build via
|
||||
@@ -39,17 +58,10 @@ if [[ ! -d out ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The .dockerignore lives under docker/, but Docker reads it from the build
|
||||
# context root. Stage it for the duration of the build so node_modules/.next
|
||||
# don't get shipped into the build context.
|
||||
cleanup() { rm -f .dockerignore; }
|
||||
trap cleanup EXIT
|
||||
cp docker/.dockerignore .dockerignore
|
||||
|
||||
echo "==> Building ${IMAGE} for linux/amd64"
|
||||
docker build \
|
||||
--platform linux/amd64 \
|
||||
-f docker/Dockerfile \
|
||||
-f "${DOCKERFILE}" \
|
||||
-t "${IMAGE}" \
|
||||
.
|
||||
|
||||
@@ -60,13 +72,13 @@ echo "==> Done: ${OUTPUT} ($(du -h "${OUTPUT}" | cut -f1))"
|
||||
echo
|
||||
echo "Copy and run on the server:"
|
||||
echo " scp ${OUTPUT} user@your-server:/tmp/"
|
||||
echo " docker load < /tmp/${OUTPUT}"
|
||||
echo " docker load < /tmp/${OUTPUT##*/}"
|
||||
echo
|
||||
echo "Run (nginx serves on container port 80). Set the runtime env vars the"
|
||||
echo "Run (nginx serves on container port ${CONTAINER_PORT}). Set the runtime env vars the"
|
||||
echo "dashboard templates at startup — for the Agent Network onboarding test,"
|
||||
echo "flip NETBIRD_AGENT_NETWORK_ONLY=true:"
|
||||
echo " docker run -d --name ${IMAGE_NAME} --restart unless-stopped \\"
|
||||
echo " -p 8080:80 \\"
|
||||
echo " -p 8080:${CONTAINER_PORT} \\"
|
||||
echo " -e NETBIRD_MGMT_API_ENDPOINT=https://your-mgmt:443 \\"
|
||||
echo " -e AUTH_AUTHORITY=https://your-idp/realms/netbird \\"
|
||||
echo " -e AUTH_CLIENT_ID=netbird-dashboard \\"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
FROM alpine:3.24
|
||||
|
||||
RUN apk add --no-cache bash ca-certificates gettext nginx tzdata && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
mkdir -p /tmp/nginx
|
||||
|
||||
STOPSIGNAL SIGINT
|
||||
EXPOSE 8080
|
||||
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
COPY docker/default.conf /etc/nginx/http.d/default.conf
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY docker/rootless.conf /etc/nginx/http.d/rootless.conf
|
||||
COPY docker/init_react_envs.sh /usr/local/init_react_envs.sh
|
||||
COPY out/ /usr/share/nginx/html/
|
||||
|
||||
RUN sed -i \
|
||||
-e 's/listen 80 default_server;/listen 8080 default_server;/' \
|
||||
-e 's/listen \[::\]:80 default_server;/listen [::]:8080 default_server;/' \
|
||||
/etc/nginx/http.d/default.conf && \
|
||||
sed -i '/^user nginx;$/d' /etc/nginx/nginx.conf && \
|
||||
chmod +x /usr/local/init_react_envs.sh && \
|
||||
chgrp -R 0 /etc/nginx/http.d /usr/share/nginx/html /tmp/nginx && \
|
||||
chmod -R g=u /etc/nginx/http.d /usr/share/nginx/html /tmp/nginx
|
||||
|
||||
USER 10001:0
|
||||
ENTRYPOINT ["/usr/local/init_react_envs.sh"]
|
||||
CMD ["/usr/sbin/nginx", "-e", "/dev/stderr", "-g", "pid /tmp/nginx.pid;"]
|
||||
+47
-11
@@ -1,23 +1,59 @@
|
||||
# NetBird Dashboard
|
||||
NetBird Dashboard is NetBirds Management server UI. It allows 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``` built on every PR being merged to the repository
|
||||
NetBird Dashboard is NetBird's management server UI.
|
||||
|
||||
## Tags
|
||||
|
||||
Each published image tag also has a rootless variant with the `-rootless`
|
||||
suffix. For example, `main` is also published as `main-rootless`.
|
||||
|
||||
## How to use this image
|
||||
HTTP run:
|
||||
|
||||
HTTP:
|
||||
|
||||
```shell
|
||||
docker run -d --rm -p 80:80 wiretrustee/dashboard:main
|
||||
docker run -d --rm -p 80:80 netbirdio/dashboard:main
|
||||
```
|
||||
Using SSL certificate from Let's Encrypt®:
|
||||
|
||||
Using an SSL certificate from Let's Encrypt:
|
||||
|
||||
```shell
|
||||
docker run -d --rm -p 80:80 -p 443:443 \
|
||||
-e LETSENCRYPT_DOMAIN=app.mydomain.com \
|
||||
-e LETSENCRYPT_EMAIL=hello@mydomain.com \
|
||||
netbirdio/dashboard:main
|
||||
```
|
||||
> For SSL generation, you need to run this image in a server with proper public IP and a domain name pointing to it.
|
||||
|
||||
For certificate generation, the server needs a public IP and a domain name
|
||||
pointing to it.
|
||||
|
||||
### Rootless and OpenShift
|
||||
|
||||
The rootless image listens on port `8080`, runs as UID `10001` by default, and
|
||||
supports OpenShift's arbitrary UID when the container retains GID `0`:
|
||||
|
||||
```shell
|
||||
docker run -d --rm \
|
||||
--user 12345:0 \
|
||||
-p 8080:8080 \
|
||||
-e AUTH_AUTHORITY=https://idp.example.com \
|
||||
-e AUTH_CLIENT_ID=netbird-dashboard \
|
||||
-e AUTH_AUDIENCE=netbird-dashboard \
|
||||
-e AUTH_SUPPORTED_SCOPES='openid profile email' \
|
||||
-e USE_AUTH0=false \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=https://api.example.com \
|
||||
netbirdio/dashboard:main-rootless
|
||||
```
|
||||
|
||||
The rootless image does not include Certbot or expose HTTPS. Terminate TLS at
|
||||
the OpenShift Route, Ingress, or another reverse proxy.
|
||||
|
||||
## Environment variables
|
||||
* ```NGINX_SSL_PORT``` Changes the port that Nginx listens to. Defaults to ```443```
|
||||
* ```LETSENCRYPT_DOMAIN``` Enables Certbot`s client execution for the specified domain. Defaults to ```none```
|
||||
* ```LETSENCRYPT_EMAIL``` Email used in Certbot`s client execution to register the certificate request. Defaults to ```example@local```
|
||||
|
||||
- `NGINX_SSL_PORT` changes the port that Nginx listens to. Defaults to `443`.
|
||||
- `LETSENCRYPT_DOMAIN` enables Certbot for the specified domain. Defaults to
|
||||
`none`.
|
||||
- `LETSENCRYPT_EMAIL` is used to register the certificate request. Defaults to
|
||||
`example@local`.
|
||||
|
||||
These certificate variables apply only to the standard image.
|
||||
@@ -173,6 +173,12 @@ for f in $(grep -R -l AUTH_SUPPORTED_SCOPES /usr/share/nginx/html); do
|
||||
rm "$f".copy
|
||||
done
|
||||
|
||||
# When a command is provided, initialization happens before the server starts.
|
||||
# The rootless image uses this path so nginx never needs to be reloaded.
|
||||
if (( $# > 0 )); then
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
# Reload nginx so the patched CSP header takes effect.
|
||||
# supervisord starts nginx (priority 100) before this script (priority 201) and
|
||||
# never reloads it, so without this nginx keeps serving the static default.conf
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# nginx includes this file inside the http context.
|
||||
# All runtime state stays in a directory writable by the arbitrary UID used by OpenShift.
|
||||
client_body_temp_path /tmp/nginx/client_temp;
|
||||
proxy_temp_path /tmp/nginx/proxy_temp;
|
||||
fastcgi_temp_path /tmp/nginx/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/nginx/uwsgi_temp;
|
||||
scgi_temp_path /tmp/nginx/scgi_temp;
|
||||
@@ -1,6 +1,7 @@
|
||||
# Ignore zitadel environment
|
||||
.env
|
||||
Caddyfile
|
||||
agentgateway-stub.Caddyfile
|
||||
management.json
|
||||
turnserver.conf
|
||||
zitadel.env
|
||||
|
||||
@@ -19,7 +19,7 @@ check_docker_compose() {
|
||||
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
|
||||
|
||||
$DOCKER_COMPOSE_COMMAND down --volumes
|
||||
rm -f docker-compose.yml Caddyfile zitadel.env .env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json proxy.env proxy-no-ports.env
|
||||
rm -f docker-compose.yml Caddyfile agentgateway-stub.Caddyfile zitadel.env .env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json proxy.env proxy-no-ports.env
|
||||
rm -rf proxy-certs proxy-certs-no-ports
|
||||
rm -f ../../.test-config.json ../playwright.env.json
|
||||
rm -f ../fixtures/auth/owner.json ../fixtures/auth/user.json
|
||||
@@ -532,7 +532,7 @@ initEnvironment() {
|
||||
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
|
||||
echo "You can use the following commands:"
|
||||
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
|
||||
echo " rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json proxy.env proxy-no-ports.env && rm -rf proxy-certs proxy-certs-no-ports"
|
||||
echo " rm -f docker-compose.yml Caddyfile agentgateway-stub.Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json proxy.env proxy-no-ports.env && rm -rf proxy-certs proxy-certs-no-ports"
|
||||
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
|
||||
exit 1
|
||||
fi
|
||||
@@ -540,6 +540,7 @@ initEnvironment() {
|
||||
echo Rendering initial files...
|
||||
renderDockerCompose > docker-compose.yml
|
||||
renderCaddyfile > Caddyfile
|
||||
renderAgentGatewayStub > agentgateway-stub.Caddyfile
|
||||
renderZitadelEnv > zitadel.env
|
||||
echo "" > turnserver.conf
|
||||
echo "" > management.json
|
||||
@@ -612,6 +613,30 @@ initEnvironment() {
|
||||
echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env
|
||||
}
|
||||
|
||||
# Saving an Agent Network provider makes management call the upstream and
|
||||
# refuses the save unless it answers a model listing, so a provider spec needs
|
||||
# a reachable OpenAI-compatible endpoint. agentgateway is self-hosted and has
|
||||
# no public one; this stub stands in for it, inside the compose network where
|
||||
# management can reach it and without depending on a vendor being up.
|
||||
renderAgentGatewayStub() {
|
||||
cat <<'EOF'
|
||||
{
|
||||
admin off
|
||||
auto_https off
|
||||
}
|
||||
|
||||
:8088 {
|
||||
handle /v1/models {
|
||||
header Content-Type application/json
|
||||
respond `{"object":"list","data":[{"id":"agentgateway-e2e-model","object":"model","owned_by":"netbird-e2e"}]}` 200
|
||||
}
|
||||
handle {
|
||||
respond 404
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
renderCaddyfile() {
|
||||
cat <<EOF
|
||||
{
|
||||
@@ -818,6 +843,15 @@ services:
|
||||
volumes:
|
||||
- netbird_caddy_data:/data
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
# Stand-in upstream for the Agent Network provider specs: answers the model
|
||||
# listing management probes before it accepts a provider save. Reachable
|
||||
# only from inside the compose network, at http://agentgateway-stub:8088.
|
||||
agentgateway-stub:
|
||||
image: caddy
|
||||
restart: unless-stopped
|
||||
networks: [ netbird ]
|
||||
volumes:
|
||||
- ./agentgateway-stub.Caddyfile:/etc/caddy/Caddyfile
|
||||
# Management
|
||||
management:
|
||||
image: ghcr.io/netbirdio/management-cloud:${MANAGEMENT_IMAGE_TAG}
|
||||
|
||||
@@ -24,6 +24,31 @@ export async function openControlCenter(page: Page, tab?: FlowView) {
|
||||
await dismissBlockingOverlays(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the groups view already showing `groupName`.
|
||||
*
|
||||
* The view picks its own group on first render (getFirstGroup: the highest
|
||||
* peers_count that is a policy source), and that ranking runs over every group
|
||||
* in the account — including ones a spec on the other worker seeded moments
|
||||
* ago. Deleting the spec's own fixtures first is not enough to pin it, so
|
||||
* select the group by name and let the view rebuild around it.
|
||||
*/
|
||||
export async function openGroupView(page: Page, groupName: string) {
|
||||
await openControlCenter(page, "groups");
|
||||
const selector = canvasNode(page, "select-group-node");
|
||||
await expect(selector).toBeVisible();
|
||||
// Unforced on purpose: the opening fitView animates the canvas, and the
|
||||
// actionability check is what waits for the node to stop moving.
|
||||
await selector.click();
|
||||
await page.getByTestId("select-dropdown-search").fill(groupName);
|
||||
await page
|
||||
.locator('[role="option"]')
|
||||
.filter({ hasText: groupName })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
await expect(selector).toContainText(groupName);
|
||||
}
|
||||
|
||||
export async function switchFlowView(page: Page, view: FlowView) {
|
||||
await dismissBlockingOverlays(page);
|
||||
await page.getByTestId(`cc-flow-${view}`).click({ force: true });
|
||||
|
||||
@@ -59,7 +59,12 @@ test.describe
|
||||
.getByTestId("agent-network-provider-option-agentgateway")
|
||||
.click({ force: true });
|
||||
|
||||
const upstreamURL = "https://agentgateway.e2e.example";
|
||||
// Saving a provider makes management call the upstream, and it refuses
|
||||
// the save with a 422 unless the upstream answers a model listing — so a
|
||||
// made-up host cannot be used here. agentgateway is self-hosted and has
|
||||
// no public endpoint to point at, so create-test-env.sh runs a stub that
|
||||
// answers that probe inside the compose network.
|
||||
const upstreamURL = "http://agentgateway-stub:8088";
|
||||
await page
|
||||
.getByTestId("agent-network-provider-upstream-url")
|
||||
.fill(upstreamURL);
|
||||
|
||||
@@ -364,13 +364,17 @@ test.describe.serial("Control Center Draft Matrix @control-center", () => {
|
||||
}) => {
|
||||
const group = await place(page, "group", 0.6, 0.5);
|
||||
await expectChangeCount(page, 1);
|
||||
// React Flow ignores delete keys while an input is focused, and the panel's
|
||||
// search takes focus on click, so blur it before pressing Backspace.
|
||||
await group.click();
|
||||
await expect(group).toHaveClass(/selected/);
|
||||
await page.evaluate(
|
||||
() => (document.activeElement as HTMLElement | null)?.blur(),
|
||||
);
|
||||
// React Flow ignores delete keys while an input is focused, and the panel
|
||||
// the click opens takes focus into its search — from an effect, about
|
||||
// 100ms later. Blurring before that lands leaves the input to claim focus
|
||||
// afterwards and swallow the Backspace, so wait for the focus, then drop
|
||||
// it.
|
||||
const panelSearch = page.locator("#cc-group-panel input").first();
|
||||
await expect(panelSearch).toBeFocused();
|
||||
await panelSearch.blur();
|
||||
await expect(panelSearch).not.toBeFocused();
|
||||
await page.keyboard.press("Backspace");
|
||||
await expect(canvasNode(page, "group-new-")).toHaveCount(0);
|
||||
await expectChangeCount(page, 0);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
dismissBlockingOverlays,
|
||||
enterDraft,
|
||||
openControlCenter,
|
||||
openGroupView,
|
||||
resetDraftState,
|
||||
reviewButton,
|
||||
} from "../helpers/control-center";
|
||||
@@ -37,7 +38,6 @@ test.describe
|
||||
await deleteGroupsByPrefix(page, PREFIX);
|
||||
});
|
||||
|
||||
// Clean-slated so the draft group view's auto-select is deterministic.
|
||||
async function seedPolicyGroupView(page: Page) {
|
||||
await deletePoliciesBySubstring(page, PREFIX);
|
||||
await deleteGroupsByPrefix(page, PREFIX);
|
||||
@@ -49,7 +49,9 @@ test.describe
|
||||
src.id,
|
||||
dst.id,
|
||||
);
|
||||
await openControlCenter(page, "groups");
|
||||
// Selected by name: the view's own pick ranks every group in the account,
|
||||
// so a peer-bearing group from another spec on the other worker wins it.
|
||||
await openGroupView(page, src.name);
|
||||
await enterDraft(page);
|
||||
const policyNode = canvasNode(page, `policy-${policy.id}`);
|
||||
await expect(policyNode).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
clickContextMenuItem,
|
||||
dismissBlockingOverlays,
|
||||
openControlCenter,
|
||||
openGroupView,
|
||||
resetDraftState,
|
||||
switchFlowView,
|
||||
} from "../helpers/control-center";
|
||||
@@ -47,8 +48,6 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
});
|
||||
|
||||
async function seedPolicyAndOpenGroupView(page: Page, enabled = true) {
|
||||
// The group view auto-selects a group that HAS a policy, so stale policies
|
||||
// would make the rendered group non-deterministic.
|
||||
await deletePoliciesBySubstring(page, PREFIX);
|
||||
await deleteGroupsByPrefix(page, PREFIX);
|
||||
|
||||
@@ -64,15 +63,10 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
);
|
||||
|
||||
const policyNode = canvasNode(page, `policy-${policy.id}`);
|
||||
// A stale SWR read or a build/fit race can fail to place the node.
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
await openControlCenter(page, "groups");
|
||||
const shown = await policyNode
|
||||
.waitFor({ state: "visible", timeout: 10_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (shown) break;
|
||||
}
|
||||
// Selecting src by name rather than trusting the view's own pick: the
|
||||
// auto-select ranks every group in the account, so a peer-bearing group
|
||||
// another spec seeded on the other worker outranks this one.
|
||||
await openGroupView(page, src.name);
|
||||
await expect(policyNode).toBeVisible({ timeout: 10_000 });
|
||||
return { src, dst, policy, policyNode };
|
||||
}
|
||||
@@ -512,7 +506,6 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
test("Should save group resource membership in live mode (correct payload)", async ({
|
||||
dashboardAsOwner: page,
|
||||
}) => {
|
||||
// Clean slate so the group view deterministically lands on our group.
|
||||
await deletePoliciesBySubstring(page, PREFIX);
|
||||
await deleteGroupsByPrefix(page, PREFIX);
|
||||
|
||||
@@ -529,7 +522,7 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
[],
|
||||
);
|
||||
|
||||
await openControlCenter(page, "groups");
|
||||
await openGroupView(page, src.name);
|
||||
const groupNode = canvasNode(page, `group-${dst.id}`);
|
||||
await expect(groupNode).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
@@ -610,7 +603,7 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
);
|
||||
await createPolicy(page, generateRandomName(PREFIX), src.id, d2.id);
|
||||
|
||||
await openControlCenter(page, "groups");
|
||||
await openGroupView(page, src.name);
|
||||
const policyNode = canvasNode(page, `policy-${p1.id}`);
|
||||
await expect(policyNode).toBeVisible({ timeout: 15_000 });
|
||||
await expect(
|
||||
|
||||
Generated
+52
-66
@@ -33,7 +33,7 @@
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/lodash": "4.17.24",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/node": "20.19.43",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-window": "^1.8.8",
|
||||
@@ -64,7 +64,6 @@
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-confetti-explosion": "^3.0.3",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-ga4": "^2.1.0",
|
||||
@@ -83,7 +82,6 @@
|
||||
"typescript": "^5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -850,23 +848,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "9.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
|
||||
"integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
|
||||
@@ -906,27 +887,40 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/types": "^0.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/node": {
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
||||
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
|
||||
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/core": "^0.19.1",
|
||||
"@humanfs/core": "^0.19.2",
|
||||
"@humanfs/types": "^0.15.0",
|
||||
"@humanwhocodes/retry": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/types": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
|
||||
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/module-importer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
@@ -3687,12 +3681,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz",
|
||||
"integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==",
|
||||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
@@ -4616,9 +4610,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"version": "4.28.8",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
||||
"integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -4635,11 +4629,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
"baseline-browser-mapping": "^2.11.12",
|
||||
"caniuse-lite": "^1.0.30001809",
|
||||
"electron-to-chromium": "^1.5.402",
|
||||
"node-releases": "^2.0.53",
|
||||
"update-browserslist-db": "^1.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -5645,9 +5639,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.302",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
||||
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
||||
"version": "1.5.420",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz",
|
||||
"integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/elkjs": {
|
||||
@@ -8430,10 +8424,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"license": "MIT"
|
||||
"version": "2.0.54",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
|
||||
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
@@ -8782,7 +8779,6 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -8941,9 +8937,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
|
||||
"integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
@@ -9078,16 +9074,6 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-confetti-explosion": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-confetti-explosion/-/react-confetti-explosion-3.0.3.tgz",
|
||||
"integrity": "sha512-ow5ns/1ttzXsIlbbfJmWJNiyQK8lTHBL6lRSUXGaK44K/3NIMngR57Ja96l+D6txTeFhfe0BfXGvORMxhtRDng==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-day-picker": {
|
||||
"version": "9.13.2",
|
||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.2.tgz",
|
||||
@@ -10508,9 +10494,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
@@ -10549,9 +10535,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
|
||||
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
||||
+7
-3
@@ -48,7 +48,7 @@
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/lodash": "4.17.24",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/node": "20.19.43",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-window": "^1.8.8",
|
||||
@@ -79,7 +79,6 @@
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-confetti-explosion": "^3.0.3",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-ga4": "^2.1.0",
|
||||
@@ -101,7 +100,6 @@
|
||||
"minimatch": ">=10.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -114,5 +112,11 @@
|
||||
"prettier": "3.0.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"allowScripts": {
|
||||
"@axa-fr/oidc-client": false,
|
||||
"@axa-fr/react-oidc": false,
|
||||
"fsevents": false,
|
||||
"unrs-resolver": false
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,10 @@ export default function NetBirdSettings() {
|
||||
<VerticalTabs.List>
|
||||
{permission.settings.read && (
|
||||
<>
|
||||
<VerticalTabs.Trigger value="authentication" data-testid="settings-tab-authentication">
|
||||
<VerticalTabs.Trigger
|
||||
value="authentication"
|
||||
data-testid="settings-tab-authentication"
|
||||
>
|
||||
<ShieldIcon size={14} />
|
||||
Authentication
|
||||
</VerticalTabs.Trigger>
|
||||
@@ -78,19 +81,31 @@ export default function NetBirdSettings() {
|
||||
Identity Providers
|
||||
</VerticalTabs.Trigger>
|
||||
)}
|
||||
<VerticalTabs.Trigger value="groups" data-testid="settings-tab-groups">
|
||||
<VerticalTabs.Trigger
|
||||
value="groups"
|
||||
data-testid="settings-tab-groups"
|
||||
>
|
||||
<FolderGit2Icon size={14} />
|
||||
Groups
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="permissions" data-testid="settings-tab-permissions">
|
||||
<VerticalTabs.Trigger
|
||||
value="permissions"
|
||||
data-testid="settings-tab-permissions"
|
||||
>
|
||||
<LockIcon size={14} />
|
||||
Permissions
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="networks" data-testid="settings-tab-networks">
|
||||
<VerticalTabs.Trigger
|
||||
value="networks"
|
||||
data-testid="settings-tab-networks"
|
||||
>
|
||||
<NetworkIcon size={14} />
|
||||
Networks
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="clients" data-testid="settings-tab-clients">
|
||||
<VerticalTabs.Trigger
|
||||
value="clients"
|
||||
data-testid="settings-tab-clients"
|
||||
>
|
||||
<MonitorSmartphoneIcon size={14} />
|
||||
Clients
|
||||
</VerticalTabs.Trigger>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useOidcAccessToken, useOidcIdToken } from "@axa-fr/react-oidc";
|
||||
import loadConfig from "@utils/config";
|
||||
import { useMemo } from "react";
|
||||
import { useJwt } from "react-jwt";
|
||||
import { decodeToken } from "react-jwt";
|
||||
|
||||
const config = loadConfig();
|
||||
export const useDomainCategory = () => {
|
||||
@@ -9,10 +9,12 @@ export const useDomainCategory = () => {
|
||||
const { idToken } = useOidcIdToken();
|
||||
const { accessToken } = useOidcAccessToken();
|
||||
const token = tokenSource.toLowerCase() == "idtoken" ? idToken : accessToken;
|
||||
const { decodedToken } = useJwt<Record<any, any>>(token);
|
||||
|
||||
const domainCategory = useMemo(() => {
|
||||
try {
|
||||
const decodedToken = token
|
||||
? decodeToken<Record<any, any>>(token)
|
||||
: undefined;
|
||||
const key = decodedToken
|
||||
? Object.keys(decodedToken)
|
||||
.filter((key) => key.includes("wt_account_domain_category"))
|
||||
@@ -22,7 +24,7 @@ export const useDomainCategory = () => {
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}, [decodedToken]);
|
||||
}, [token]);
|
||||
|
||||
const isPrivate = useMemo(() => {
|
||||
return domainCategory === "private";
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Card from "@components/Card";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { SkeletonNotificationSettings } from "@components/skeletons/SkeletonNotificationSettings";
|
||||
import { SmallBadge } from "@components/ui/SmallBadge";
|
||||
import { VerticalTabs } from "@components/VerticalTabs";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { ExternalLinkIcon, MessageSquareDot } from "lucide-react";
|
||||
import React from "react";
|
||||
import SettingsIcon from "@/assets/icons/SettingsIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useIsLicensed } from "@/hooks/useIsLicensed";
|
||||
import { VerticalTabs } from "@components/VerticalTabs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Card from "@components/Card";
|
||||
import { NotificationEmailChannel } from "@/cloud/notifications/channels/NotificationEmailChannel";
|
||||
import { NotificationSlackChannel } from "@/cloud/notifications/channels/NotificationSlackChannel";
|
||||
import { NotificationWebhookChannel } from "@/cloud/notifications/channels/NotificationWebhookChannel";
|
||||
import useUrlTab from "@/hooks/useUrlTab";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { NotificationChannelListItem } from "@/cloud/notifications/NotificationChannelListItem";
|
||||
import NotificationProvider, {
|
||||
useNotifications,
|
||||
} from "@/cloud/notifications/NotificationProvider";
|
||||
import { NotificationChannelListItem } from "@/cloud/notifications/NotificationChannelListItem";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useIsLicensed } from "@/hooks/useIsLicensed";
|
||||
import useUrlTab from "@/hooks/useUrlTab";
|
||||
import {
|
||||
NOTIFICATION_CHANNELS_DOCS_LINK,
|
||||
NotificationChannelType,
|
||||
} from "@/interfaces/NotificationChannel";
|
||||
import { SkeletonNotificationSettings } from "@components/skeletons/SkeletonNotificationSettings";
|
||||
import { NotificationSlackChannel } from "@/cloud/notifications/channels/NotificationSlackChannel";
|
||||
import { SmallBadge } from "@components/ui/SmallBadge";
|
||||
|
||||
const NotificationsOverview = ({
|
||||
onSelectChannel,
|
||||
@@ -52,23 +52,18 @@ const NotificationsOverview = ({
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<div className={"flex items-start justify-between -mt-1"}>
|
||||
<div>
|
||||
<h1>Notifications</h1>
|
||||
<Paragraph>
|
||||
Choose how to be notified when important events occur in your
|
||||
account.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about{" "}
|
||||
<Paragraph className={"block"}>
|
||||
Get notified when important events happen.{" "}
|
||||
<InlineLink
|
||||
href={NOTIFICATION_CHANNELS_DOCS_LINK}
|
||||
target={"_blank"}
|
||||
>
|
||||
Notification Channels
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loaded 20 of 69 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user