@ -0,0 +1,55 @@ |
|||||||
|
name: Run BATS Tests |
||||||
|
|
||||||
|
on: |
||||||
|
push: |
||||||
|
paths: |
||||||
|
- 'docker-compose/setup-script/noco.sh' |
||||||
|
workflow_dispatch: |
||||||
|
|
||||||
|
jobs: |
||||||
|
prepare: |
||||||
|
runs-on: ubuntu-latest |
||||||
|
outputs: |
||||||
|
matrix: ${{ steps.set-matrix.outputs.matrix }} |
||||||
|
steps: |
||||||
|
- name: Checkout code |
||||||
|
uses: actions/checkout@v4 |
||||||
|
|
||||||
|
- name: Install jq |
||||||
|
run: | |
||||||
|
sudo apt-get update |
||||||
|
sudo apt-get install -y jq |
||||||
|
|
||||||
|
- name: Prepare matrix for test files |
||||||
|
id: set-matrix |
||||||
|
run: | |
||||||
|
BATS_FILES=$(find docker-compose/setup-script/tests -name '*.bats') |
||||||
|
MATRIX_JSON=$(echo $BATS_FILES | jq -Rsc 'split("\n") | map(select(. != ""))') |
||||||
|
echo "matrix=$MATRIX_JSON" >> $GITHUB_ENV |
||||||
|
|
||||||
|
test: |
||||||
|
needs: prepare |
||||||
|
runs-on: ubuntu-latest |
||||||
|
strategy: |
||||||
|
fail-fast: false |
||||||
|
matrix: |
||||||
|
test: ${{fromJson(env.matrix)}} |
||||||
|
steps: |
||||||
|
- name: Checkout repository |
||||||
|
uses: actions/checkout@v4 |
||||||
|
|
||||||
|
- name: Install BATS |
||||||
|
run: | |
||||||
|
sudo apt-get update |
||||||
|
sudo apt-get install -y bats expect |
||||||
|
|
||||||
|
- name: Get working directory |
||||||
|
run: | |
||||||
|
WORKING_DIR="$(pwd)/docker-compose/setup-script/tests" |
||||||
|
echo "WORKING_DIR=$WORKING_DIR" >> $GITHUB_ENV |
||||||
|
|
||||||
|
- name: Run BATS test |
||||||
|
run: bats ${{ matrix.test }} |
||||||
|
env: |
||||||
|
WORKING_DIR: ${{ env.WORKING_DIR }} |
||||||
|
SKIP_TARE_DOWN: true |
@ -0,0 +1,31 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/configure" || exit 1 |
||||||
|
./setup.sh "setup" |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Properly runs monitor script" { |
||||||
|
../expects/configure/restart.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" || exit 1 |
||||||
|
|
||||||
|
# Verify container is running |
||||||
|
docker compose ps | grep -q 'redis' |
||||||
|
docker compose ps | grep -q 'watchtower' |
||||||
|
docker compose ps | grep -q 'nocodb' |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/configure" || exit 1 |
||||||
|
./setup.sh "setup" |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check all containers are restarted" { |
||||||
|
../expects/configure/restart.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" || exit 1 |
||||||
|
|
||||||
|
# Verify container is running |
||||||
|
docker compose ps | grep -q 'redis' |
||||||
|
docker compose ps | grep -q 'watchtower' |
||||||
|
docker compose ps | grep -q 'nocodb' |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/configure" || exit 1 |
||||||
|
./setup.sh "setup" |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check NocoDB is scaled to 3 instances" { |
||||||
|
nproc() { |
||||||
|
echo 4 |
||||||
|
} |
||||||
|
|
||||||
|
../expects/configure/scale.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" || exit 1 |
||||||
|
|
||||||
|
result=$(docker compose ps | grep -c "nocodb/nocodb") |
||||||
|
[ "${result}" -eq 3 ] |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
if [ -z "$NOCO_HOME" ]; then |
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
fi |
||||||
|
|
||||||
|
if [ -d "$NOCO_HOME" ]; then |
||||||
|
cd "$NOCO_HOME" || exit |
||||||
|
docker compose down |
||||||
|
fi |
||||||
|
|
||||||
|
cd "$WORKING_DIR" || exit |
||||||
|
rm -rf "$NOCO_HOME" |
||||||
|
|
||||||
|
if [ "$1" = "setup" ]; then |
||||||
|
../noco.sh <<< $'\n\nN\n' |
||||||
|
fi |
@ -0,0 +1,31 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/configure" || exit 1 |
||||||
|
./setup.sh "setup" |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check all containers are up" { |
||||||
|
../expects/configure/start.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" || exit 1 |
||||||
|
|
||||||
|
# Verify container is running |
||||||
|
docker compose ps | grep -q 'redis' |
||||||
|
docker compose ps | grep -q 'watchtower' |
||||||
|
docker compose ps | grep -q 'nocodb' |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/configure" || exit 1 |
||||||
|
./setup.sh setup |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check all containers are down" { |
||||||
|
../expects/configure/stop.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" || exit 1 |
||||||
|
|
||||||
|
# Verify container is not running |
||||||
|
count=$(docker compose ps -q | wc -l) |
||||||
|
[ "$count" -eq 0 ] |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/configure" || exit 1 |
||||||
|
./setup.sh "setup" |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check all containers are upgraded" { |
||||||
|
../expects/configure/upgrade.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" || exit 1 |
||||||
|
|
||||||
|
# Verify container is running |
||||||
|
docker compose ps | grep -q 'redis' |
||||||
|
docker compose ps | grep -q 'watchtower' |
||||||
|
docker compose ps | grep -q 'nocodb' |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
expect "Do you want to reinstall NocoDB*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "7\r" |
||||||
|
|
||||||
|
send \x03 |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "0\r" |
||||||
|
|
||||||
|
expect EOF |
@ -0,0 +1,20 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
expect "Do you want to reinstall NocoDB*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "4\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "0\r" |
||||||
|
|
||||||
|
expect EOF |
@ -0,0 +1,23 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
expect "Do you want to reinstall NocoDB*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "6\r" |
||||||
|
|
||||||
|
expect "How many instances of NocoDB do you want to run*" |
||||||
|
send "3\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "0\r" |
||||||
|
|
||||||
|
expect EOF |
@ -0,0 +1,20 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
expect "Do you want to reinstall NocoDB*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "1\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "0\r" |
||||||
|
|
||||||
|
expect EOF |
@ -0,0 +1,20 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
expect "Do you want to reinstall NocoDB*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "2\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "0\r" |
||||||
|
|
||||||
|
expect EOF |
@ -0,0 +1,20 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
expect "Do you want to reinstall NocoDB*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "5\r" |
||||||
|
|
||||||
|
expect "Enter your choice: " |
||||||
|
send "0\r" |
||||||
|
|
||||||
|
expect EOF |
@ -0,0 +1,21 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
# Respond to script prompts |
||||||
|
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Show Advanced Options*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to start the management menu*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect eof |
@ -0,0 +1,22 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
# shellcheck shell=bash |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
# Respond to script prompts |
||||||
|
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||||
|
send "192.168.1.10\r" |
||||||
|
|
||||||
|
expect "Show Advanced Options*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to start the management menu*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect eof |
@ -0,0 +1,33 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
# Respond to script prompts |
||||||
|
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Show Advanced Options*" |
||||||
|
send "Y\r" |
||||||
|
|
||||||
|
expect "Choose Community or Enterprise Edition*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to enabled Redis for caching*" |
||||||
|
send "Y\r" |
||||||
|
|
||||||
|
expect "Do you want to enabled Watchtower for automatic updates*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "How many instances of NocoDB do you want to run*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to start the management menu*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect eof |
@ -0,0 +1,33 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
# Respond to script prompts |
||||||
|
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Show Advanced Options*" |
||||||
|
send "Y\r" |
||||||
|
|
||||||
|
expect "Choose Community or Enterprise Edition*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to enabled Redis for caching*" |
||||||
|
send "Y\r" |
||||||
|
|
||||||
|
expect "Do you want to enabled Watchtower for automatic updates*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "How many instances of NocoDB do you want to run*" |
||||||
|
send "2\r" |
||||||
|
|
||||||
|
expect "Do you want to start the management menu*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect eof |
@ -0,0 +1,38 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
set random_number [lindex $argv 0] |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
# Respond to script prompts |
||||||
|
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||||
|
send "${random_number}.ssl.nocodb.dev\r" |
||||||
|
|
||||||
|
expect "Show Advanced Options*" |
||||||
|
send "y\r" |
||||||
|
|
||||||
|
expect "Do you want to configure SSL*" |
||||||
|
send "y\r" |
||||||
|
|
||||||
|
expect "Choose Community or Enterprise Edition*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to enabled Redis for caching*" |
||||||
|
send "Y\r" |
||||||
|
|
||||||
|
expect "Do you want to enabled Watchtower for automatic updates*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "How many instances of NocoDB do you want to run*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to start the management menu*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect eof |
@ -0,0 +1,33 @@ |
|||||||
|
#!/usr/bin/expect -f |
||||||
|
|
||||||
|
# Configure timeout for each expect command |
||||||
|
set timeout 10 |
||||||
|
|
||||||
|
# Start your main script |
||||||
|
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||||
|
|
||||||
|
spawn bash ../../noco.sh |
||||||
|
|
||||||
|
# Respond to script prompts |
||||||
|
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Show Advanced Options*" |
||||||
|
send "Y\r" |
||||||
|
|
||||||
|
expect "Choose Community or Enterprise Edition*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to enabled Redis for caching*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to enabled Watchtower for automatic updates*" |
||||||
|
send "Y\r" |
||||||
|
|
||||||
|
expect "How many instances of NocoDB do you want to run*" |
||||||
|
send "\r" |
||||||
|
|
||||||
|
expect "Do you want to start the management menu*" |
||||||
|
send "N\r" |
||||||
|
|
||||||
|
expect eof |
@ -0,0 +1,36 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check installation with all default options" { |
||||||
|
../expects/install/default.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" |
||||||
|
|
||||||
|
# Check Docker Compose file to verify configuration |
||||||
|
grep -q 'redis' docker-compose.yml |
||||||
|
grep -q 'watchtower' docker-compose.yml |
||||||
|
grep -q 'nocodb' docker-compose.yml |
||||||
|
|
||||||
|
# Verify container is running |
||||||
|
docker compose ps | grep -q 'redis' |
||||||
|
docker compose ps | grep -q 'watchtower' |
||||||
|
docker compose ps | grep -q 'nocodb' |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check installation with custom ip" { |
||||||
|
../expects/install/ip.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" |
||||||
|
|
||||||
|
# Check Docker Compose file to verify configuration |
||||||
|
grep -q 'redis' docker-compose.yml |
||||||
|
grep -q 'watchtower' docker-compose.yml |
||||||
|
grep -q 'nocodb' docker-compose.yml |
||||||
|
|
||||||
|
# Verify container is running |
||||||
|
docker compose ps | grep -q 'redis' |
||||||
|
docker compose ps | grep -q 'watchtower' |
||||||
|
docker compose ps | grep -q 'nocodb' |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check Redis is enabled when specified" { |
||||||
|
../expects/install/redis.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" |
||||||
|
|
||||||
|
# Check Docker Compose file to verify Redis configuration |
||||||
|
grep -q 'redis' docker-compose.yml |
||||||
|
|
||||||
|
# Verify Redis container is running |
||||||
|
docker compose ps | grep -q 'redis' |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check if two instances of NoCoDB can be run" { |
||||||
|
../expects/install/scale.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" |
||||||
|
|
||||||
|
# Get scale from docker compose ps |
||||||
|
scale=$(docker compose ps | grep -c "nocodb/nocodb") |
||||||
|
[ "$scale" -eq 2 ] |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
if [ -z "$NOCO_HOME" ]; then |
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
fi |
||||||
|
|
||||||
|
if [ -d "$NOCO_HOME" ]; then |
||||||
|
cd "$NOCO_HOME" || exit |
||||||
|
docker compose down |
||||||
|
fi |
||||||
|
|
||||||
|
rm -rf "$NOCO_HOME" |
@ -0,0 +1,30 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
RANDOM_NUMBER=$RANDOM |
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Should create SSL certificates" { |
||||||
|
if [ -z "$TEST_SSL" ] |
||||||
|
then |
||||||
|
skip "Skipping SSL tests" |
||||||
|
fi |
||||||
|
|
||||||
|
../expects/install/ssl.sh "$RANDOM_NUMBER" |
||||||
|
|
||||||
|
curl -ksS --head "https://${RANDOM_NUMBER}.ssl.nocodb.dev" > /dev/null |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
#!/usr/bin/env bats |
||||||
|
|
||||||
|
NOCO_HOME="${HOME}/.nocodb" |
||||||
|
export NOCO_HOME |
||||||
|
|
||||||
|
setup() { |
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
teardown() { |
||||||
|
if [ -n "$SKIP_TEARDOWN" ]; then |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
cd "${WORKING_DIR}/install" || exit 1 |
||||||
|
./setup.sh |
||||||
|
} |
||||||
|
|
||||||
|
@test "Check WatchTower is enabled when specified" { |
||||||
|
../expects/install/watchtower.sh |
||||||
|
|
||||||
|
cd "${NOCO_HOME}" |
||||||
|
|
||||||
|
# Check Docker Compose file to verify WatchTower configuration |
||||||
|
grep -q 'watchtower' docker-compose.yml |
||||||
|
|
||||||
|
# Verify WatchTower container is running |
||||||
|
docker compose ps | grep -q 'watchtower' |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
echo "--- Clear Mock ---" |
@ -0,0 +1,3 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
echo 4 |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 377 B |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 686 B After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 732 B |
After Width: | Height: | Size: 347 B |
After Width: | Height: | Size: 257 B |
@ -0,0 +1,32 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
const { header, field, toggleSort } = defineProps<{ |
||||||
|
header: string |
||||||
|
activeSort: { field?: string; direction?: string } |
||||||
|
field: UsersSortType['field'] |
||||||
|
toggleSort: Function |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex items-center space-x-2 cursor-pointer text-gray-700" @click="toggleSort(field)"> |
||||||
|
<span> |
||||||
|
{{ header }} |
||||||
|
</span> |
||||||
|
<div class="flex flex-col"> |
||||||
|
<GeneralIcon |
||||||
|
icon="arrowDropUp" |
||||||
|
class="text-sm mb-[-10px] text-[16px]" |
||||||
|
:class="{ |
||||||
|
'text-primary': activeSort.field === field && activeSort.direction === 'asc', |
||||||
|
}" |
||||||
|
/> |
||||||
|
<GeneralIcon |
||||||
|
icon="arrowDropDown" |
||||||
|
class="text-sm text-[16px]" |
||||||
|
:class="{ |
||||||
|
'text-primary': activeSort.field === field && activeSort.direction === 'desc', |
||||||
|
}" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -1,79 +0,0 @@ |
|||||||
<script lang="ts" setup> |
|
||||||
import { iconMap } from '#imports' |
|
||||||
import type { UsersSortType } from '~/lib' |
|
||||||
|
|
||||||
const { field, direction, handleUserSort } = defineProps<{ |
|
||||||
field: UsersSortType['field'] |
|
||||||
direction?: UsersSortType['direction'] |
|
||||||
handleUserSort: Function |
|
||||||
}>() |
|
||||||
|
|
||||||
const isOpen = ref(false) |
|
||||||
|
|
||||||
const sortUserBy = (direction?: UsersSortType['direction']) => { |
|
||||||
handleUserSort({ |
|
||||||
field, |
|
||||||
direction, |
|
||||||
}) |
|
||||||
isOpen.value = false |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<a-dropdown |
|
||||||
v-model:visible="isOpen" |
|
||||||
:trigger="['click']" |
|
||||||
placement="bottomLeft" |
|
||||||
overlay-class-name="nc-user-menu-column-operations !border-1 rounded-lg !shadow-xl" |
|
||||||
@click.stop="isOpen = !isOpen" |
|
||||||
> |
|
||||||
<div> |
|
||||||
<GeneralIcon |
|
||||||
:icon="direction === 'asc' || direction === 'desc' ? 'sortDesc' : 'arrowDown'" |
|
||||||
class="text-grey h-full text-grey nc-user-menu-trigger cursor-pointer outline-0 mr-2 transition-none" |
|
||||||
:style="{ transform: direction === 'asc' ? 'rotate(180deg)' : undefined }" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
<template #overlay> |
|
||||||
<NcMenu class="flex flex-col gap-1 border-gray-200 nc-user-menu-column-options"> |
|
||||||
<NcMenuItem @click="sortUserBy('asc')"> |
|
||||||
<div class="nc-column-insert-after nc-user-menu-item"> |
|
||||||
<component |
|
||||||
:is="iconMap.sortDesc" |
|
||||||
class="text-gray-700 !rotate-180 !w-4.25 !h-4.25" |
|
||||||
:style="{ |
|
||||||
transform: 'rotate(180deg)', |
|
||||||
}" |
|
||||||
/> |
|
||||||
|
|
||||||
<!-- Sort Ascending --> |
|
||||||
{{ $t('general.sortAsc') }} |
|
||||||
</div> |
|
||||||
</NcMenuItem> |
|
||||||
<NcMenuItem @click="sortUserBy('desc')"> |
|
||||||
<div class="nc-column-insert-before nc-user-menu-item"> |
|
||||||
<component :is="iconMap.sortDesc" class="text-gray-700 !w-4.25 !h-4.25 ml-0.5 mr-0.25" /> |
|
||||||
<!-- Sort Descending --> |
|
||||||
{{ $t('general.sortDesc') }} |
|
||||||
</div> |
|
||||||
</NcMenuItem> |
|
||||||
</NcMenu> |
|
||||||
</template> |
|
||||||
</a-dropdown> |
|
||||||
</template> |
|
||||||
|
|
||||||
<style scoped> |
|
||||||
.nc-user-menu-item { |
|
||||||
@apply flex items-center gap-2; |
|
||||||
} |
|
||||||
|
|
||||||
.nc-user-menu-column-options { |
|
||||||
.nc-icons { |
|
||||||
@apply !w-5 !h-5; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
:deep(.ant-dropdown-menu-item) { |
|
||||||
@apply !hover:text-black text-gray-700; |
|
||||||
} |
|
||||||
</style> |
|
@ -0,0 +1,3 @@ |
|||||||
|
<template> |
||||||
|
<span></span> |
||||||
|
</template> |
@ -0,0 +1,110 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import { useVModel } from '#imports' |
||||||
|
|
||||||
|
interface Prop { |
||||||
|
modelValue: boolean |
||||||
|
extensionId: string |
||||||
|
from: 'market' | 'extension' |
||||||
|
} |
||||||
|
|
||||||
|
const props = defineProps<Prop>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const vModel = useVModel(props, 'modelValue', emit) |
||||||
|
|
||||||
|
const { availableExtensions, addExtension, getExtensionIcon, isMarketVisible } = useExtensions() |
||||||
|
|
||||||
|
const onBack = () => { |
||||||
|
vModel.value = false |
||||||
|
isMarketVisible.value = true |
||||||
|
} |
||||||
|
|
||||||
|
const onAddExtension = (ext: any) => { |
||||||
|
addExtension(ext) |
||||||
|
vModel.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const activeExtension = computed(() => { |
||||||
|
return availableExtensions.value.find((ext) => ext.id === props.extensionId) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcModal |
||||||
|
v-model:visible="vModel" |
||||||
|
:body-style="{ 'max-height': '864px', 'height': '85vh' }" |
||||||
|
:class="{ active: vModel }" |
||||||
|
:closable="from === 'extension'" |
||||||
|
:footer="null" |
||||||
|
:width="1280" |
||||||
|
size="medium" |
||||||
|
wrap-class-name="nc-modal-extension-market" |
||||||
|
> |
||||||
|
<div v-if="activeExtension" class="flex flex-col w-full h-full"> |
||||||
|
<div v-if="from === 'market'" class="h-[40px] flex items-start"> |
||||||
|
<div class="flex items-center gap-2 pr-2 pb-2 cursor-pointer hover:text-primary" @click="onBack"> |
||||||
|
<GeneralIcon icon="ncArrowLeft" /> |
||||||
|
<span>Back</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-else class="h-[40px]"></div> |
||||||
|
<div class="extension-details"> |
||||||
|
<div class="extension-details-left"> |
||||||
|
<div class="flex"> |
||||||
|
<img :src="getExtensionIcon(activeExtension.iconUrl)" alt="icon" class="h-[90px]" /> |
||||||
|
<div class="flex flex-col p-4"> |
||||||
|
<div class="font-weight-700 text-2xl">{{ activeExtension.title }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="p-4"> |
||||||
|
<div class="whitespace-pre-line">{{ activeExtension.description }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="extension-details-right"> |
||||||
|
<NcButton class="w-full" @click="onAddExtension(activeExtension)"> |
||||||
|
<div class="flex items-center justify-center">Add Extension</div> |
||||||
|
</NcButton> |
||||||
|
<div class="flex flex-col gap-1"> |
||||||
|
<div class="text-md font-weight-600">Version</div> |
||||||
|
<div>{{ activeExtension.version }}</div> |
||||||
|
</div> |
||||||
|
<div class="flex flex-col gap-1"> |
||||||
|
<div v-if="activeExtension.publisherName" class="text-md font-weight-600">Publisher</div> |
||||||
|
<div>{{ activeExtension.publisherName }}</div> |
||||||
|
</div> |
||||||
|
<div v-if="activeExtension.publisherEmail" class="flex flex-col gap-1"> |
||||||
|
<div class="text-md font-weight-600">Publisher Email</div> |
||||||
|
<div> |
||||||
|
<a :href="`mailto:${activeExtension.publisherEmail}`" target="_blank" rel="noopener noreferrer"> |
||||||
|
{{ activeExtension.publisherEmail }} |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-if="activeExtension.publisherUrl" class="flex flex-col gap-1"> |
||||||
|
<div class="text-md font-weight-600">Publisher Website</div> |
||||||
|
<div> |
||||||
|
<a :href="activeExtension.publisherUrl" target="_blank" rel="noopener noreferrer"> |
||||||
|
{{ activeExtension.publisherUrl }} |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</NcModal> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.extension-details { |
||||||
|
@apply flex w-full h-full; |
||||||
|
|
||||||
|
.extension-details-left { |
||||||
|
@apply flex flex-col w-3/4 p-2; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-details-right { |
||||||
|
@apply w-1/4 p-2 flex flex-col gap-4; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,231 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
interface Prop { |
||||||
|
extensionId: string |
||||||
|
error?: any |
||||||
|
} |
||||||
|
|
||||||
|
const { extensionId, error } = defineProps<Prop>() |
||||||
|
|
||||||
|
const { extensionList, extensionsLoaded, availableExtensions, getExtensionIcon, duplicateExtension, showExtensionDetails } = |
||||||
|
useExtensions() |
||||||
|
|
||||||
|
const activeError = ref(error) |
||||||
|
|
||||||
|
const extensionModalRef = ref<HTMLElement>() |
||||||
|
|
||||||
|
const extension = computed(() => { |
||||||
|
const ext = extensionList.value.find((ext) => ext.id === extensionId) |
||||||
|
if (!ext) { |
||||||
|
throw new Error('Extension not found') |
||||||
|
} |
||||||
|
return ext |
||||||
|
}) |
||||||
|
|
||||||
|
const titleInput = ref<HTMLInputElement | null>(null) |
||||||
|
|
||||||
|
const titleEditMode = ref<boolean>(false) |
||||||
|
|
||||||
|
const tempTitle = ref<string>(extension.value.title) |
||||||
|
|
||||||
|
const enableEditMode = () => { |
||||||
|
titleEditMode.value = true |
||||||
|
tempTitle.value = extension.value.title |
||||||
|
nextTick(() => { |
||||||
|
titleInput.value?.focus() |
||||||
|
titleInput.value?.select() |
||||||
|
titleInput.value?.scrollIntoView() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const updateExtensionTitle = async () => { |
||||||
|
await extension.value.setTitle(tempTitle.value) |
||||||
|
titleEditMode.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const { fullscreen, collapsed } = useProvideExtensionHelper(extension) |
||||||
|
|
||||||
|
const component = ref<any>(null) |
||||||
|
|
||||||
|
const extensionManifest = ref<any>(null) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
until(extensionsLoaded) |
||||||
|
.toMatch((v) => v) |
||||||
|
.then(() => { |
||||||
|
extensionManifest.value = availableExtensions.value.find((ext) => ext.id === extension.value.extensionId) |
||||||
|
|
||||||
|
if (!extensionManifest) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
import(`../../extensions/${extensionManifest.value.entry}/index.vue`).then((mod) => { |
||||||
|
component.value = markRaw(mod.default) |
||||||
|
}) |
||||||
|
}) |
||||||
|
.catch((err) => { |
||||||
|
if (!extensionManifest.value) { |
||||||
|
activeError.value = 'There was an error loading the extension' |
||||||
|
return |
||||||
|
} |
||||||
|
activeError.value = err |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
// close fullscreen on escape key press |
||||||
|
useEventListener('keydown', (e) => { |
||||||
|
if (e.key === 'Escape') { |
||||||
|
fullscreen.value = false |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// close fullscreen on clicking extensionModalRef directly |
||||||
|
const closeFullscreen = (e: MouseEvent) => { |
||||||
|
if (e.target === extensionModalRef.value) { |
||||||
|
fullscreen.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="w-full p-2"> |
||||||
|
<div class="extension-wrapper"> |
||||||
|
<div class="extension-header"> |
||||||
|
<div class="extension-header-left"> |
||||||
|
<GeneralIcon icon="drag" /> |
||||||
|
<img v-if="extensionManifest" :src="getExtensionIcon(extensionManifest.iconUrl)" alt="icon" class="h-6" /> |
||||||
|
<input |
||||||
|
v-if="titleEditMode" |
||||||
|
ref="titleInput" |
||||||
|
v-model="tempTitle" |
||||||
|
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent w-4/5" |
||||||
|
@click.stop |
||||||
|
@keyup.enter="updateExtensionTitle" |
||||||
|
@keyup.esc="updateExtensionTitle" |
||||||
|
@blur="updateExtensionTitle" |
||||||
|
/> |
||||||
|
<div v-else class="extension-title" @dblclick="enableEditMode">{{ extension.title }}</div> |
||||||
|
</div> |
||||||
|
<div class="extension-header-right"> |
||||||
|
<GeneralIcon v-if="!activeError" icon="expand" @click="fullscreen = true" /> |
||||||
|
<NcDropdown :trigger="['click']"> |
||||||
|
<GeneralIcon icon="threeDotVertical" /> |
||||||
|
|
||||||
|
<template #overlay> |
||||||
|
<NcMenu> |
||||||
|
<template v-if="!activeError"> |
||||||
|
<NcMenuItem data-rec="true" class="!hover:text-primary" @click="enableEditMode"> |
||||||
|
<GeneralIcon icon="edit" /> |
||||||
|
Rename |
||||||
|
</NcMenuItem> |
||||||
|
<NcMenuItem data-rec="true" class="!hover:text-primary" @click="duplicateExtension(extension.id)"> |
||||||
|
<GeneralIcon icon="duplicate" /> |
||||||
|
Duplicate |
||||||
|
</NcMenuItem> |
||||||
|
<NcMenuItem |
||||||
|
data-rec="true" |
||||||
|
class="!hover:text-primary" |
||||||
|
@click="showExtensionDetails(extension.extensionId, 'extension')" |
||||||
|
> |
||||||
|
<GeneralIcon icon="info" /> |
||||||
|
Details |
||||||
|
</NcMenuItem> |
||||||
|
<NcDivider /> |
||||||
|
</template> |
||||||
|
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="extension.clear()"> |
||||||
|
<GeneralIcon icon="reload" /> |
||||||
|
Clear Data |
||||||
|
</NcMenuItem> |
||||||
|
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="extension.delete()"> |
||||||
|
<GeneralIcon icon="delete" /> |
||||||
|
Delete |
||||||
|
</NcMenuItem> |
||||||
|
</NcMenu> |
||||||
|
</template> |
||||||
|
</NcDropdown> |
||||||
|
<GeneralIcon v-if="collapsed" icon="arrowUp" @click="collapsed = !collapsed" /> |
||||||
|
<GeneralIcon v-else icon="arrowDown" @click="collapsed = !collapsed" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<template v-if="activeError"> |
||||||
|
<div v-show="!collapsed" class="extension-content"> |
||||||
|
<a-result status="error" title="Extension Error"> |
||||||
|
<template #subTitle>{{ activeError }}</template> |
||||||
|
<template #extra> |
||||||
|
<NcButton @click="extension.clear()"> |
||||||
|
<div class="flex items-center gap-2"> |
||||||
|
<GeneralIcon icon="reload" /> |
||||||
|
Clear Data |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
<NcButton type="danger" @click="extension.delete()"> |
||||||
|
<div class="flex items-center gap-2"> |
||||||
|
<GeneralIcon icon="delete" /> |
||||||
|
Delete |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</template> |
||||||
|
</a-result> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<Teleport to="body" :disabled="!fullscreen"> |
||||||
|
<div ref="extensionModalRef" :class="{ 'extension-modal': fullscreen }" @click="closeFullscreen"> |
||||||
|
<div :class="{ 'extension-modal-content': fullscreen }"> |
||||||
|
<div |
||||||
|
v-if="fullscreen" |
||||||
|
class="flex items-center justify-between p-2 bg-gray-100 rounded-t-lg cursor-default h-[40px]" |
||||||
|
> |
||||||
|
<div class="flex items-center gap-2 text-gray-500 font-weight-600"> |
||||||
|
<img v-if="extensionManifest" :src="getExtensionIcon(extensionManifest.iconUrl)" alt="icon" class="w-6 h-6" /> |
||||||
|
<div class="text-sm">{{ extension.title }}</div> |
||||||
|
</div> |
||||||
|
<GeneralIcon class="cursor-pointer" icon="close" @click="fullscreen = false" /> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
v-show="fullscreen || !collapsed" |
||||||
|
class="extension-content" |
||||||
|
:class="{ 'border-1': !fullscreen, 'h-[calc(100%-40px)]': fullscreen }" |
||||||
|
> |
||||||
|
<component :is="component" :key="extension.uiKey" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Teleport> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.extension-wrapper { |
||||||
|
@apply bg-white rounded-lg p-2 w-full border-1; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-header { |
||||||
|
@apply flex justify-between mb-2; |
||||||
|
|
||||||
|
.extension-header-left { |
||||||
|
@apply flex items-center gap-2; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-header-right { |
||||||
|
@apply flex items-center gap-4; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-title { |
||||||
|
@apply font-weight-600; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.extension-content { |
||||||
|
@apply rounded-lg; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-modal { |
||||||
|
@apply absolute top-0 left-0 z-50 w-full h-full bg-black bg-opacity-50; |
||||||
|
|
||||||
|
.extension-modal-content { |
||||||
|
@apply bg-white rounded-lg w-[90%] h-[90vh] mt-[5vh] mx-auto; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,72 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import { useVModel } from '#imports' |
||||||
|
|
||||||
|
interface Prop { |
||||||
|
modelValue?: boolean |
||||||
|
} |
||||||
|
|
||||||
|
const props = defineProps<Prop>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const vModel = useVModel(props, 'modelValue', emit) |
||||||
|
|
||||||
|
const { availableExtensions, addExtension, getExtensionIcon, showExtensionDetails } = useExtensions() |
||||||
|
|
||||||
|
const onExtensionClick = (extensionId: string) => { |
||||||
|
showExtensionDetails(extensionId) |
||||||
|
vModel.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const onAddExtension = (ext: any) => { |
||||||
|
addExtension(ext) |
||||||
|
vModel.value = false |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcModal |
||||||
|
v-model:visible="vModel" |
||||||
|
:body-style="{ 'max-height': '864px', 'height': '85vh' }" |
||||||
|
:class="{ active: vModel }" |
||||||
|
:closable="true" |
||||||
|
:footer="null" |
||||||
|
:width="1280" |
||||||
|
size="medium" |
||||||
|
wrap-class-name="nc-modal-extension-market" |
||||||
|
> |
||||||
|
<div class="flex flex-col h-full"> |
||||||
|
<div class="flex items-center px-4 py-2"> |
||||||
|
<div class="flex items-center gap-2"> |
||||||
|
<GeneralIcon icon="puzzle" /> |
||||||
|
<div class="font-weight-700">Extensions Marketplace</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="flex flex-col flex-1 px-4 py-2"> |
||||||
|
<div class="flex flex-wrap gap-4 p-2"> |
||||||
|
<template v-for="ext of availableExtensions" :key="ext.id"> |
||||||
|
<div class="flex border-1 rounded-lg p-2 w-[360px] cursor-pointer" @click="onExtensionClick(ext.id)"> |
||||||
|
<div class="h-[60px] overflow-hidden m-auto"> |
||||||
|
<img :src="getExtensionIcon(ext.iconUrl)" alt="icon" class="w-full h-full object-cover" /> |
||||||
|
</div> |
||||||
|
<div class="flex flex-grow flex-col ml-3"> |
||||||
|
<div class="flex justify-between"> |
||||||
|
<div class="font-weight-600">{{ ext.title }}</div> |
||||||
|
<NcButton size="xsmall" @click.stop="onAddExtension(ext)"> |
||||||
|
<div class="flex items-center gap-1 mx-1"> |
||||||
|
<GeneralIcon icon="plus" /> |
||||||
|
Add |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
<div class="w-[250px] h-[50px] text-xs line-clamp-3">{{ ext.description }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</NcModal> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped></style> |
@ -0,0 +1,59 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { Pane } from 'splitpanes' |
||||||
|
import 'splitpanes/dist/splitpanes.css' |
||||||
|
|
||||||
|
const { extensionList, isPanelExpanded, isDetailsVisible, detailsExtensionId, detailsFrom, isMarketVisible, extensionPanelSize } = |
||||||
|
useExtensions() |
||||||
|
|
||||||
|
const toggleMarket = () => { |
||||||
|
isMarketVisible.value = !isMarketVisible.value |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<Pane v-if="isPanelExpanded" :size="extensionPanelSize" class="flex flex-col bg-orange-50"> |
||||||
|
<div class="flex items-center pl-3 pt-3 font-weight-800 text-orange-500">Extensions</div> |
||||||
|
<template v-if="extensionList.length === 0"> |
||||||
|
<div class="flex items-center flex-col gap-2 w-full nc-scrollbar-md"> |
||||||
|
<div class="w-[100px] h-[100px] bg-gray-200 rounded-lg mt-[100px]"></div> |
||||||
|
<div class="font-weight-700">No extensions added</div> |
||||||
|
<div>Add Extensions from the community extensions marketplace</div> |
||||||
|
<NcButton @click="toggleMarket"> |
||||||
|
<div class="flex items-center gap-2 font-weight-600"> |
||||||
|
<GeneralIcon icon="plus" /> |
||||||
|
Add Extension |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<div class="flex w-full items-center justify-between py-2 px-2 bg-orange-50"> |
||||||
|
<div class="flex flex-grow items-center mr-2"> |
||||||
|
<a-input type="text" class="!h-8 !px-3 !py-1 !rounded-lg" placeholder="Search Extension"> |
||||||
|
<template #prefix> |
||||||
|
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" /> |
||||||
|
</template> |
||||||
|
</a-input> |
||||||
|
</div> |
||||||
|
<NcButton type="ghost" size="small" class="!text-primary !bg-white" @click="toggleMarket"> |
||||||
|
<div class="flex items-center gap-1 px-1 text-xs"> |
||||||
|
<GeneralIcon icon="plus" /> |
||||||
|
Add Extension |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
<div class="flex items-center flex-col w-full nc-scrollbar-md"> |
||||||
|
<ExtensionsWrapper v-for="ext in extensionList" :key="ext.id" :extension-id="ext.id" /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<ExtensionsMarket v-if="isMarketVisible" v-model="isMarketVisible" /> |
||||||
|
<ExtensionsDetails |
||||||
|
v-if="isDetailsVisible && detailsExtensionId" |
||||||
|
v-model="isDetailsVisible" |
||||||
|
:extension-id="detailsExtensionId" |
||||||
|
:from="detailsFrom" |
||||||
|
/> |
||||||
|
</Pane> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss"></style> |
@ -0,0 +1,18 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
interface Prop { |
||||||
|
extensionId: string |
||||||
|
} |
||||||
|
|
||||||
|
const { extensionId } = defineProps<Prop>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NuxtErrorBoundary> |
||||||
|
<ExtensionsExtension :extension-id="extensionId" /> |
||||||
|
<template #error="{ error }"> |
||||||
|
<ExtensionsExtension :extension-id="extensionId" :error="error" /> |
||||||
|
</template> |
||||||
|
</NuxtErrorBoundary> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"></style> |
@ -0,0 +1,26 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { useCopy } from '~/composables/useCopy' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
content?: string |
||||||
|
timeout?: number |
||||||
|
}>() |
||||||
|
|
||||||
|
const { copy } = useCopy() |
||||||
|
const copied = ref(false) |
||||||
|
|
||||||
|
const copyContent = async () => { |
||||||
|
await copy(props.content || '') |
||||||
|
copied.value = true |
||||||
|
setTimeout(() => { |
||||||
|
copied.value = false |
||||||
|
}, props.timeout || 2000) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcButton size="xsmall" type="text" @click="copyContent"> |
||||||
|
<MdiCheck v-if="copied" class="h-3.5" /> |
||||||
|
<component :is="iconMap.copy" v-else class="text-gray-800" /> |
||||||
|
</NcButton> |
||||||
|
</template> |