mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-23 09:15:31 +00:00
39
.github/workflows/restart-runners.yml
vendored
39
.github/workflows/restart-runners.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
name: Restart Runners
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 */7 * *'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ec2-maintenance-prod:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: write-all
|
|
||||||
steps:
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
|
||||||
with:
|
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
|
||||||
role-duration-seconds: 3600
|
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
|
||||||
|
|
||||||
- name: Verify AWS identity
|
|
||||||
run: aws sts get-caller-identity
|
|
||||||
|
|
||||||
- name: Start EC2 instance
|
|
||||||
run: |
|
|
||||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
|
||||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
|
||||||
echo "EC2 instances started"
|
|
||||||
|
|
||||||
- name: Wait
|
|
||||||
run: sleep 600
|
|
||||||
|
|
||||||
- name: Stop EC2 instance
|
|
||||||
run: |
|
|
||||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
|
||||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
|
||||||
echo "EC2 instances stopped"
|
|
||||||
160
.github/workflows/saas.yml
vendored
160
.github/workflows/saas.yml
vendored
@@ -1,160 +0,0 @@
|
|||||||
name: SAAS Pipeline
|
|
||||||
|
|
||||||
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
|
||||||
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write # for GHCR push
|
|
||||||
id-token: write # for Cosign Keyless (OIDC) Signing
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "[0-9]+.[0-9]+.[0-9]+-s.[0-9]+"
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pre-run:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: write-all
|
|
||||||
steps:
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
|
||||||
with:
|
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
|
||||||
role-duration-seconds: 3600
|
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
|
||||||
|
|
||||||
- name: Verify AWS identity
|
|
||||||
run: aws sts get-caller-identity
|
|
||||||
|
|
||||||
- name: Start EC2 instances
|
|
||||||
run: |
|
|
||||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
|
||||||
echo "EC2 instances started"
|
|
||||||
|
|
||||||
|
|
||||||
release-arm:
|
|
||||||
name: Build and Release (ARM64)
|
|
||||||
runs-on: [self-hosted, linux, arm64, us-east-1]
|
|
||||||
needs: [pre-run]
|
|
||||||
if: >-
|
|
||||||
${{
|
|
||||||
needs.pre-run.result == 'success'
|
|
||||||
}}
|
|
||||||
# Job-level timeout to avoid runaway or stuck runs
|
|
||||||
timeout-minutes: 120
|
|
||||||
env:
|
|
||||||
# Target images
|
|
||||||
AWS_IMAGE: ${{ secrets.aws_account_id }}.dkr.ecr.us-east-1.amazonaws.com/${{ github.event.repository.name }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
|
|
||||||
- name: Download MaxMind GeoLite2 databases
|
|
||||||
env:
|
|
||||||
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
|
|
||||||
run: |
|
|
||||||
echo "Downloading MaxMind GeoLite2 databases..."
|
|
||||||
|
|
||||||
# Download GeoLite2-Country
|
|
||||||
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
|
||||||
-o GeoLite2-Country.tar.gz
|
|
||||||
|
|
||||||
# Download GeoLite2-ASN
|
|
||||||
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
|
||||||
-o GeoLite2-ASN.tar.gz
|
|
||||||
|
|
||||||
# Extract the .mmdb files
|
|
||||||
tar -xzf GeoLite2-Country.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
|
||||||
tar -xzf GeoLite2-ASN.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
|
||||||
|
|
||||||
# Verify files exist
|
|
||||||
if [ ! -f "GeoLite2-Country.mmdb" ]; then
|
|
||||||
echo "ERROR: Failed to download GeoLite2-Country.mmdb"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "GeoLite2-ASN.mmdb" ]; then
|
|
||||||
echo "ERROR: Failed to download GeoLite2-ASN.mmdb"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up tar files
|
|
||||||
rm -f GeoLite2-Country.tar.gz GeoLite2-ASN.tar.gz
|
|
||||||
|
|
||||||
echo "MaxMind databases downloaded successfully"
|
|
||||||
ls -lh GeoLite2-*.mmdb
|
|
||||||
|
|
||||||
- name: Monitor storage space
|
|
||||||
run: |
|
|
||||||
THRESHOLD=75
|
|
||||||
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
|
||||||
echo "Used space: $USED_SPACE%"
|
|
||||||
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
|
||||||
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
|
||||||
echo y | docker system prune -a
|
|
||||||
else
|
|
||||||
echo "Storage space is above the threshold. No action needed."
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
|
||||||
with:
|
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
|
||||||
role-duration-seconds: 3600
|
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
|
||||||
|
|
||||||
- name: Login to Amazon ECR
|
|
||||||
id: login-ecr
|
|
||||||
uses: aws-actions/amazon-ecr-login@v2
|
|
||||||
|
|
||||||
- name: Extract tag name
|
|
||||||
id: get-tag
|
|
||||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Update version in package.json
|
|
||||||
run: |
|
|
||||||
TAG=${{ env.TAG }}
|
|
||||||
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
|
||||||
cat server/lib/consts.ts
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Build and push Docker images (Docker Hub - ARM64)
|
|
||||||
run: |
|
|
||||||
TAG=${{ env.TAG }}
|
|
||||||
make build-saas tag=$TAG
|
|
||||||
echo "Built & pushed ARM64 images to: ${{ env.AWS_IMAGE }}:${TAG}"
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
post-run:
|
|
||||||
needs: [pre-run, release-arm]
|
|
||||||
if: >-
|
|
||||||
${{
|
|
||||||
always() &&
|
|
||||||
needs.pre-run.result == 'success' &&
|
|
||||||
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure')
|
|
||||||
}}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: write-all
|
|
||||||
steps:
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
|
||||||
with:
|
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
|
||||||
role-duration-seconds: 3600
|
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
|
||||||
|
|
||||||
- name: Verify AWS identity
|
|
||||||
run: aws sts get-caller-identity
|
|
||||||
|
|
||||||
- name: Stop EC2 instances
|
|
||||||
run: |
|
|
||||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
|
||||||
echo "EC2 instances stopped"
|
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -68,6 +69,9 @@ const (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
|
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
|
||||||
|
|
||||||
fmt.Println("Welcome to the Pangolin installer!")
|
fmt.Println("Welcome to the Pangolin installer!")
|
||||||
@@ -206,7 +210,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !checkIsCrowdsecInstalledInCompose() {
|
if *crowdsecFlag && !checkIsCrowdsecInstalledInCompose() {
|
||||||
fmt.Println("\n=== CrowdSec Install ===")
|
fmt.Println("\n=== CrowdSec Install ===")
|
||||||
// check if crowdsec is installed
|
// check if crowdsec is installed
|
||||||
if readBool("Would you like to install CrowdSec?", false) {
|
if readBool("Would you like to install CrowdSec?", false) {
|
||||||
|
|||||||
@@ -61,14 +61,14 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
|
|||||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{client.online ? (
|
{client.online ? (
|
||||||
<div className="text-green-500 flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t("online")}</span>
|
<span>{t("connected")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-neutral-500 flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("disconnected")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) {
|
|||||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{remoteExitNode.online ? (
|
{remoteExitNode.online ? (
|
||||||
<div className="text-green-500 flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t("online")}</span>
|
<span>{t("online")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-neutral-500 flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,14 +140,14 @@ export default function ExitNodesTable({
|
|||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
if (originalRow.online) {
|
if (originalRow.online) {
|
||||||
return (
|
return (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t("online")}</span>
|
<span>{t("online")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -519,21 +519,21 @@ export default function HealthChecksTable({
|
|||||||
const health = row.original.hcHealth;
|
const health = row.original.hcHealth;
|
||||||
if (health === "healthy") {
|
if (health === "healthy") {
|
||||||
return (
|
return (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full" />
|
<div className="w-2 h-2 bg-green-500 rounded-full" />
|
||||||
<span>{t("standaloneHcHealthStateHealthy")}</span>
|
<span>{t("standaloneHcHealthStateHealthy")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else if (health === "unhealthy") {
|
} else if (health === "unhealthy") {
|
||||||
return (
|
return (
|
||||||
<span className="text-red-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-red-500 rounded-full" />
|
<div className="w-2 h-2 bg-red-500 rounded-full" />
|
||||||
<span>{t("standaloneHcHealthStateUnhealthy")}</span>
|
<span>{t("standaloneHcHealthStateUnhealthy")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full" />
|
<div className="w-2 h-2 bg-neutral-500 rounded-full" />
|
||||||
<span>{t("standaloneHcHealthStateUnknown")}</span>
|
<span>{t("standaloneHcHealthStateUnknown")}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -285,14 +285,14 @@ export default function MachineClientsTable({
|
|||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
if (originalRow.online) {
|
if (originalRow.online) {
|
||||||
return (
|
return (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t("connected")}</span>
|
<span>{t("connected")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("disconnected")}</span>
|
<span>{t("disconnected")}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -228,14 +228,14 @@ export default function PendingSitesTable({
|
|||||||
) {
|
) {
|
||||||
if (originalRow.online) {
|
if (originalRow.online) {
|
||||||
return (
|
return (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t("online")}</span>
|
<span>{t("online")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -89,12 +89,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
|||||||
<InfoSectionTitle>Socket</InfoSectionTitle>
|
<InfoSectionTitle>Socket</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{isAvailable ? (
|
{isAvailable ? (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>Online</span>
|
<span>Online</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>Offline</span>
|
<span>Offline</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
|||||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{site.online ? (
|
{site.online ? (
|
||||||
<div className="text-green-500 flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t("online")}</span>
|
<span>{t("online")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-neutral-500 flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -226,14 +226,14 @@ export default function SitesTable({
|
|||||||
) {
|
) {
|
||||||
if (originalRow.online) {
|
if (originalRow.online) {
|
||||||
return (
|
return (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t("online")}</span>
|
<span>{t("online")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -436,14 +436,14 @@ export default function UserDevicesTable({
|
|||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
if (originalRow.online) {
|
if (originalRow.online) {
|
||||||
return (
|
return (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t("connected")}</span>
|
<span>{t("connected")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("disconnected")}</span>
|
<span>{t("disconnected")}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user