@ -0,0 +1,62 @@
|
||||
name: "NestJS Unit Test" |
||||
on: |
||||
push: |
||||
branches: [develop] |
||||
paths: |
||||
- "packages/nocodb/**" |
||||
- ".github/workflows/jest-unit-test.yml" |
||||
pull_request: |
||||
types: [opened, reopened, synchronize, ready_for_review, labeled] |
||||
branches: [develop] |
||||
paths: |
||||
- "packages/nocodb/**" |
||||
- ".github/workflows/jest-unit-test.yml" |
||||
workflow_call: |
||||
# Triggered manually |
||||
workflow_dispatch: |
||||
jobs: |
||||
jest-unit-test: |
||||
runs-on: [self-hosted, aws] |
||||
timeout-minutes: 20 |
||||
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft || inputs.force == true }} |
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@v3 |
||||
with: |
||||
fetch-depth: 0 |
||||
- name: Setup Node |
||||
uses: actions/setup-node@v3 |
||||
with: |
||||
node-version: 18.19.1 |
||||
- name: Setup pnpm |
||||
uses: pnpm/action-setup@v4 |
||||
with: |
||||
version: 8 |
||||
- name: Get pnpm store directory |
||||
shell: bash |
||||
timeout-minutes: 1 |
||||
run: | |
||||
echo "STORE_PATH=/root/setup-pnpm/node_modules/.bin/store/v3" >> $GITHUB_ENV |
||||
- uses: actions/cache@v3 |
||||
name: Setup pnpm cache |
||||
with: |
||||
path: ${{ env.STORE_PATH }} |
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} |
||||
restore-keys: | |
||||
${{ runner.os }}-pnpm-store- |
||||
- name: Set CI env |
||||
run: export CI=true |
||||
- name: Set NC Edition |
||||
run: export EE=true |
||||
- name: remove use-node-version line from .npmrc |
||||
run: sed -i '/^use-node-version/d' .npmrc |
||||
- name: install dependencies |
||||
run: pnpm bootstrap |
||||
- name: build nocodb-sdk |
||||
working-directory: ./packages/nocodb-sdk |
||||
run: | |
||||
pnpm run generate:sdk |
||||
pnpm run build:main |
||||
- name: run unit tests |
||||
working-directory: ./packages/nocodb |
||||
run: pnpm run test |
@ -0,0 +1,156 @@
|
||||
name: "Release : Secret CLI NPM & Executables" |
||||
|
||||
on: |
||||
# Triggered manually |
||||
workflow_dispatch: |
||||
inputs: |
||||
tag: |
||||
description: "Tag name" |
||||
required: true |
||||
secrets: |
||||
# Replace with `NC_GITHUB_TOKEN` once replaced with a token which have access to `nocodb/nc-secret-mgr` |
||||
NC_GITHUB_TOKEN_TEMP: |
||||
required: true |
||||
jobs: |
||||
build-and-publish: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
- name: Setup pnpm |
||||
uses: pnpm/action-setup@v4 |
||||
with: |
||||
version: 8 |
||||
- name: Setup Node 18.19.1 |
||||
# Setup .npmrc file to publish to npm |
||||
uses: actions/setup-node@v3 |
||||
with: |
||||
node-version: 18.19.1 |
||||
registry-url: 'https://registry.npmjs.org' |
||||
|
||||
- name: Cache pkg modules |
||||
id: cache-pkg |
||||
uses: actions/cache@v3 |
||||
env: |
||||
cache-name: cache-pkg |
||||
with: |
||||
# pkg cache files are stored in `~/.pkg-cache` |
||||
path: ~/.pkg-cache |
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} |
||||
restore-keys: | |
||||
${{ runner.os }}-build-${{ env.cache-name }}- |
||||
${{ runner.os }}-build- |
||||
${{ runner.os }}- |
||||
|
||||
- name: Npm package build and publish |
||||
env: |
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
||||
run: | |
||||
pnpm bootstrap |
||||
cd ./packages/nocodb |
||||
pnpm run build:cli:module |
||||
cd ../nc-secret-mgr |
||||
targetVersion=${{ github.event.inputs.tag || inputs.tag }} node ../../scripts/updateCliVersion.js |
||||
pnpm run build && pnpm run npm:publish |
||||
|
||||
# for building images for all platforms these libraries are required in Linux |
||||
- name: Install QEMU and ldid |
||||
run: | |
||||
sudo apt update |
||||
# Install qemu |
||||
sudo apt install qemu binfmt-support qemu-user-static -y |
||||
# install ldid |
||||
git clone https://github.com/daeken/ldid.git |
||||
cd ./ldid |
||||
./make.sh |
||||
sudo cp ./ldid /usr/local/bin |
||||
|
||||
- uses: actions/setup-node@v3 |
||||
with: |
||||
node-version: 16 |
||||
|
||||
- name : Install nocodb, other dependencies and build executables |
||||
run: | |
||||
cd ./packages/nc-secret-mgr |
||||
|
||||
# install npm dependendencies |
||||
pnpm i |
||||
|
||||
# Build sqlite binaries for all platforms |
||||
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --fallback-to-build --target_arch=x64 --target_libc=unknown |
||||
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --fallback-to-build --target_arch=ia32 --target_libc=unknown |
||||
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --fallback-to-build --target_arch=x64 --target_libc=unknown |
||||
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --fallback-to-build --target_arch=arm64 --target_libc=unknown |
||||
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=x64 --target_libc=glibc |
||||
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=arm64 --target_libc=glibc |
||||
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=x64 --target_libc=musl |
||||
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=arm64 --target_libc=musl |
||||
|
||||
# clean up code to optimize size |
||||
npx modclean --patterns="default:*" --run |
||||
|
||||
# build executables |
||||
npm run build:pkg |
||||
|
||||
ls ./dist-pkg |
||||
|
||||
# Move macOS executables for signing |
||||
mkdir ./mac-dist |
||||
mv ./dist-pkg/nc-secret-mgr-macos-arm64 ./mac-dist/ |
||||
mv ./dist-pkg/nc-secret-mgr-macos-x64 ./mac-dist/ |
||||
|
||||
- name: Upload executables(except mac executables) to release |
||||
uses: svenstaro/upload-release-action@v2 |
||||
with: |
||||
repo_token: ${{ secrets.NC_GITHUB_TOKEN_TEMP }} |
||||
file: packages/nc-secret-mgr/dist-pkg/** |
||||
tag: ${{ github.event.inputs.tag || inputs.tag }} |
||||
overwrite: true |
||||
file_glob: true |
||||
repo_name: nocodb/nc-secret-mgr |
||||
|
||||
- uses: actions/upload-artifact@master |
||||
with: |
||||
name: ${{ github.event.inputs.tag || inputs.tag }} |
||||
path: packages/nc-secret-mgr/mac-dist |
||||
retention-days: 1 |
||||
sign-mac-executables: |
||||
runs-on: macos-latest |
||||
needs: build-and-publish |
||||
steps: |
||||
|
||||
- uses: actions/download-artifact@master |
||||
with: |
||||
name: ${{ github.event.inputs.tag || inputs.tag }} |
||||
path: packages/nc-secret-mgr/mac-dist |
||||
|
||||
- name: Sign macOS executables |
||||
run: | |
||||
/usr/bin/codesign --force -s - ./packages/nc-secret-mgr/mac-dist/nc-secret-mgr-macos-arm64 -v |
||||
/usr/bin/codesign --force -s - ./packages/nc-secret-mgr/mac-dist/nc-secret-mgr-macos-x64 -v |
||||
|
||||
- uses: actions/upload-artifact@master |
||||
with: |
||||
name: ${{ format('{0}-signed', github.event.inputs.tag || inputs.tag) }} |
||||
path: packages/nc-secret-mgr/mac-dist |
||||
retention-days: 1 |
||||
|
||||
|
||||
publish-mac-executables: |
||||
needs: [sign-mac-executables,build-and-publish] |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/download-artifact@master |
||||
with: |
||||
name: ${{ format('{0}-signed', github.event.inputs.tag || inputs.tag) }} |
||||
path: packages/nc-secret-mgr/mac-dist |
||||
|
||||
- name: Upload executables(except mac executables) to release |
||||
uses: svenstaro/upload-release-action@v2 |
||||
with: |
||||
repo_token: ${{ secrets.NC_GITHUB_TOKEN_TEMP }} |
||||
file: packages/nc-secret-mgr/mac-dist/** |
||||
tag: ${{ github.event.inputs.tag || inputs.tag }} |
||||
overwrite: true |
||||
file_glob: true |
||||
repo_name: nocodb/nc-secret-mgr |
||||
|
@ -0,0 +1,28 @@
|
||||
name: "Validate: Docs" |
||||
|
||||
on: |
||||
# Triggered manually |
||||
workflow_dispatch: |
||||
pull_request: |
||||
types: [opened, reopened, synchronize, ready_for_review, labeled] |
||||
branches: [develop] |
||||
paths: |
||||
- "packages/noco-docs/**" |
||||
|
||||
jobs: |
||||
validate-docs: |
||||
runs-on: [self-hosted, aws] |
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@v3 |
||||
with: |
||||
fetch-depth: 0 |
||||
- uses: actions/setup-node@v3 |
||||
with: |
||||
node-version: 18.19.1 |
||||
- name: Build docs |
||||
run: | |
||||
cd packages/noco-docs |
||||
npm install |
||||
npm run generate |
||||
npm run remark:once |
@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env bash |
||||
|
||||
|
||||
read -p "Enter your domain name: " domain |
||||
read -p "Enter your email id: " email |
||||
|
||||
# Docker installation |
||||
if [ -x "$(command -v docker)" ]; then |
||||
echo "Docker already available" |
||||
else |
||||
sudo apt-get update |
||||
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common |
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -- |
||||
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian buster stable" |
||||
sudo apt-get update |
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io |
||||
sudo usermod -a -G docker $USER |
||||
echo "Docker installed successfully" |
||||
fi |
||||
|
||||
# Docker compose installation |
||||
if [ -x "$(command -v docker-compose)" ]; then |
||||
echo "Docker-compose already available" |
||||
else |
||||
sudo apt-get -y install wget |
||||
sudo wget https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m) -O /usr/local/bin/docker-compose |
||||
sudo chmod +x /usr/local/bin/docker-compose |
||||
docker-compose --version |
||||
echo "Docker-compose installed successfully" |
||||
fi |
||||
|
||||
|
||||
#wget https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion/archive/master.zip -O master.zip |
||||
# |
||||
#unzip -n master.zip |
||||
# |
||||
#cd docker-compose-letsencrypt-nginx-proxy-companion-master |
||||
|
||||
git clone https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion.git |
||||
|
||||
cd docker-compose-letsencrypt-nginx-proxy-companion |
||||
|
||||
OUTPUT1=$(./start.sh) |
||||
|
||||
|
||||
|
||||
|
||||
docker run -p 8080:8080 -p 8081:8081 -p 8082:8082 -d --name xc-instant \ |
||||
-e VIRTUAL_HOST="$domain" \ |
||||
-e LETSENCRYPT_HOST="$domain" \ |
||||
-e LETSENCRYPT_EMAIL="$email" \ |
||||
-e VIRTUAL_PORT=8080 \ |
||||
--network=webproxy nocodb/nocodb:latest |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,39 +0,0 @@
|
||||
version: "2.1" |
||||
services: |
||||
nocodb: |
||||
depends_on: |
||||
root_db: |
||||
condition: service_healthy |
||||
environment: |
||||
NC_DB: "mysql2://root_db:3306?u=noco&p=password&d=root_db" |
||||
image: "nocodb/nocodb:latest" |
||||
ports: |
||||
- "8080:8080" |
||||
restart: always |
||||
volumes: |
||||
- "nc_data:/usr/app/data" |
||||
root_db: |
||||
environment: |
||||
MYSQL_DATABASE: root_db |
||||
MYSQL_PASSWORD: password |
||||
MYSQL_ROOT_PASSWORD: password |
||||
MYSQL_USER: noco |
||||
healthcheck: |
||||
retries: 10 |
||||
test: |
||||
- CMD |
||||
- mysqladmin |
||||
- ping |
||||
- "-h" |
||||
- localhost |
||||
timeout: 20s |
||||
image: "mysql:8.3.0" |
||||
restart: always |
||||
volumes: |
||||
- "db_data:/var/lib/mysql" |
||||
# below line shows how to change charset and collation |
||||
# uncomment it if necessary |
||||
# command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci |
||||
volumes: |
||||
db_data: {} |
||||
nc_data: {} |
@ -1,948 +0,0 @@
|
||||
#!/bin/bash |
||||
|
||||
set -e |
||||
|
||||
# Constants |
||||
NOCO_HOME="./nocodb" |
||||
CURRENT_PATH=$(pwd) |
||||
REQUIRED_PORTS=(80 443) |
||||
|
||||
# Color definitions |
||||
RED='\033[0;31m' |
||||
GREEN='\033[0;32m' |
||||
YELLOW='\033[1;33m' |
||||
BLUE='\033[0;34m' |
||||
MAGENTA='\033[0;35m' |
||||
CYAN='\033[0;36m' |
||||
ORANGE='\033[0;33m' |
||||
BOLD='\033[1m' |
||||
NC='\033[0m' |
||||
|
||||
# Global variables |
||||
CONFIG_DOMAIN_NAME="" |
||||
CONFIG_SSL_ENABLED="" |
||||
CONFIG_EDITION="" |
||||
CONFIG_LICENSE_KEY="" |
||||
CONFIG_REDIS_ENABLED="" |
||||
CONFIG_MINIO_ENABLED="" |
||||
CONFIG_MINIO_DOMAIN_NAME="" |
||||
CONFIG_MINIO_SSL_ENABLED="" |
||||
CONFIG_WATCHTOWER_ENABLED="" |
||||
CONFIG_NUM_INSTANCES="" |
||||
CONFIG_POSTGRES_PASSWORD="" |
||||
CONFIG_REDIS_PASSWORD="" |
||||
CONFIG_MINIO_ACCESS_KEY="" |
||||
CONFIG_MINIO_ACCESS_SECRET="" |
||||
CONFIG_DOCKER_COMMAND="" |
||||
|
||||
declare -a message_arr |
||||
|
||||
# Utility functions |
||||
print_color() { printf "${1}%s${NC}\n" "$2"; } |
||||
print_info() { print_color "$BLUE" "INFO: $1"; } |
||||
print_success() { print_color "$GREEN" "SUCCESS: $1"; } |
||||
print_warning() { print_color "$YELLOW" "WARNING: $1"; } |
||||
print_error() { print_color "$RED" "ERROR: $1"; } |
||||
|
||||
print_box_message() { |
||||
local message=("$@") |
||||
local edge="======================================" |
||||
local padding=" " |
||||
|
||||
echo "$edge" |
||||
for element in "${message[@]}"; do |
||||
echo "${padding}${element}" |
||||
done |
||||
echo "$edge" |
||||
} |
||||
|
||||
print_note() { |
||||
local note_text="$1" |
||||
local note_color='\033[0;33m' # Yellow color |
||||
local bold='\033[1m' |
||||
local reset='\033[0m' |
||||
|
||||
echo -e "${note_color}${bold}NOTE:${reset} ${note_text}" |
||||
} |
||||
|
||||
command_exists() { command -v "$1" >/dev/null 2>&1; } |
||||
|
||||
is_valid_domain() { |
||||
local domain_regex="^([a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])?\.[a-zA-Z]{2,}$" |
||||
[[ "$1" =~ $domain_regex ]] |
||||
} |
||||
|
||||
urlencode() { |
||||
local string="$1" |
||||
local strlen=${#string} |
||||
local encoded="" |
||||
local pos c o |
||||
|
||||
for (( pos=0 ; pos<strlen ; pos++ )); do |
||||
c=${string:$pos:1} |
||||
case "$c" in |
||||
[-_.~a-zA-Z0-9] ) o="$c" ;; |
||||
* ) printf -v o '%%%02X' "'$c" |
||||
esac |
||||
encoded+="$o" |
||||
done |
||||
echo "$encoded" |
||||
} |
||||
|
||||
generate_password() { |
||||
openssl rand -base64 48 | tr -dc 'a-zA-Z0-9!@#$%^&*()-_+=' | head -c 32 |
||||
} |
||||
|
||||
get_public_ip() { |
||||
local ip |
||||
|
||||
if command -v dig >/dev/null 2>&1; then |
||||
ip=$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null) |
||||
if [ -n "$ip" ]; then |
||||
echo "$ip" |
||||
return |
||||
fi |
||||
fi |
||||
|
||||
# Method 2: Using curl |
||||
if command -v curl >/dev/null 2>&1; then |
||||
ip=$(curl -s -4 https://ifconfig.co 2>/dev/null) |
||||
if [ -n "$ip" ]; then |
||||
echo "$ip" |
||||
return |
||||
fi |
||||
fi |
||||
|
||||
# Method 3: Using wget |
||||
if command -v wget >/dev/null 2>&1; then |
||||
ip=$(wget -qO- https://ifconfig.me 2>/dev/null) |
||||
if [ -n "$ip" ]; then |
||||
echo "$ip" |
||||
return |
||||
fi |
||||
fi |
||||
|
||||
# Method 4: Using host |
||||
if command -v host >/dev/null 2>&1; then |
||||
ip=$(host myip.opendns.com resolver1.opendns.com 2>/dev/null | grep "myip.opendns.com has" | awk '{print $4}') |
||||
if [ -n "$ip" ]; then |
||||
echo "$ip" |
||||
return |
||||
fi |
||||
fi |
||||
|
||||
# If all methods fail, return localhost |
||||
echo "localhost" |
||||
} |
||||
|
||||
get_nproc() { |
||||
# Try to get the number of processors using nproc |
||||
if command -v nproc &> /dev/null; then |
||||
nproc |
||||
else |
||||
# Fallback: Check if /proc/cpuinfo exists and count the number of processors |
||||
if [[ -f /proc/cpuinfo ]]; then |
||||
grep -c ^processor /proc/cpuinfo |
||||
# Fallback for macOS or BSD systems using sysctl |
||||
elif command -v sysctl &> /dev/null; then |
||||
sysctl -n hw.ncpu |
||||
# Default to 1 processor if everything else fails |
||||
else |
||||
echo 1 |
||||
fi |
||||
fi |
||||
} |
||||
|
||||
|
||||
prompt() { |
||||
local prompt_text="$1" |
||||
local default_value="$2" |
||||
local response |
||||
|
||||
if [ -n "$default_value" ]; then |
||||
prompt_text+=" (default: $default_value)" |
||||
fi |
||||
prompt_text+=": " |
||||
|
||||
read -r -p "$prompt_text" response |
||||
if [ -z "$response" ] && [ -n "$default_value" ]; then |
||||
echo "$default_value" |
||||
else |
||||
echo "$response" |
||||
fi |
||||
} |
||||
|
||||
prompt_required() { |
||||
local prompt_text="$1" |
||||
local response |
||||
|
||||
while true; do |
||||
read -r -p "$prompt_text: " response |
||||
if [ -n "$response" ]; then |
||||
echo "$response" |
||||
return |
||||
fi |
||||
print_error "This field is required." |
||||
done |
||||
} |
||||
|
||||
prompt_number() { |
||||
local prompt_text="$1" |
||||
local min="$2" |
||||
local max="$3" |
||||
local response |
||||
|
||||
while true; do |
||||
read -r -p "$prompt_text ($min-$max): " response |
||||
if [[ "$response" =~ ^[0-9]+$ ]] && [ "$response" -ge "$min" ] && [ "$response" -le "$max" ]; then |
||||
echo "$response" |
||||
return |
||||
fi |
||||
print_error "Please enter a number between $min and $max." |
||||
done |
||||
} |
||||
|
||||
confirm() { |
||||
local prompt_text="$1" |
||||
local default_response="${2:-N}" |
||||
local response |
||||
|
||||
if [ "$default_response" = "Y" ] || [ "$default_response" = "y" ]; then |
||||
prompt_text+=" [Y/n]: " |
||||
else |
||||
prompt_text+=" [y/N]: " |
||||
fi |
||||
|
||||
read -r -p "$prompt_text" response |
||||
response="${response:-$default_response}" |
||||
|
||||
if [ "$response" = "Y" ] || [ "$response" = "y" ]; then |
||||
return 0 |
||||
else |
||||
return 1 |
||||
fi |
||||
} |
||||
|
||||
|
||||
generate_contact_email() { |
||||
local domain="$1" |
||||
local email |
||||
|
||||
if [ -z "$domain" ] || [ "$domain" = "localhost" ] || [[ "$domain" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then |
||||
email="contact@example.com" |
||||
else |
||||
domain="${domain#http://}" |
||||
domain="${domain#https://}" |
||||
domain="${domain%%/*}" |
||||
domain="${domain%%\?*}" |
||||
|
||||
if [[ "$domain" =~ [^.]+\.[^.]+$ ]]; then |
||||
main_domain="${BASH_REMATCH[0]}" |
||||
else |
||||
main_domain="$domain" |
||||
fi |
||||
|
||||
email="contact@$main_domain" |
||||
fi |
||||
|
||||
echo "$email" |
||||
} |
||||
|
||||
install_package() { |
||||
if command_exists yum; then |
||||
sudo yum install -y "$1" |
||||
elif command_exists apt; then |
||||
sudo apt install -y "$1" |
||||
elif command_exists brew; then |
||||
brew install "$1" |
||||
else |
||||
print_error "Package manager not found. Please install $1 manually." |
||||
exit 1 |
||||
fi |
||||
} |
||||
|
||||
add_to_hosts() { |
||||
local IP="127.0.0.1" |
||||
local HOSTS_FILE="/etc/hosts" |
||||
local TEMP_HOSTS_FILE="/tmp/hosts.tmp" |
||||
|
||||
|
||||
if is_valid_domain $CONFIG_MINIO_DOMAIN_NAME; then |
||||
return 0 |
||||
elif sudo grep -q "${CONFIG_MINIO_DOMAIN_NAME}" "$HOSTS_FILE"; then |
||||
return 0 |
||||
else |
||||
sudo cp "$HOSTS_FILE" "$TEMP_HOSTS_FILE" |
||||
echo "$IP ${CONFIG_MINIO_DOMAIN_NAME}" | sudo tee -a "$TEMP_HOSTS_FILE" > /dev/null |
||||
if sudo mv "$TEMP_HOSTS_FILE" "$HOSTS_FILE"; then |
||||
print_info "Added ${CONFIG_MINIO_DOMAIN_NAME} to $HOSTS_FILE" |
||||
print_note "You may need to reboot your system, If the uploaded attachments are not accessible." |
||||
|
||||
else |
||||
print_error "Failed to update $HOSTS_FILE. Please check your permissions." |
||||
return 1 |
||||
fi |
||||
fi |
||||
} |
||||
|
||||
check_for_docker_sudo() { |
||||
if docker ps >/dev/null 2>&1; then |
||||
echo "n" |
||||
else |
||||
echo "y" |
||||
fi |
||||
} |
||||
|
||||
read_number() { |
||||
local prompt="$1" |
||||
local default="$2" |
||||
local number |
||||
|
||||
while true; do |
||||
if [ -n "$default" ]; then |
||||
read -rp "$prompt [$default]: " number |
||||
number=${number:-$default} |
||||
else |
||||
read -rp "$prompt: " number |
||||
fi |
||||
|
||||
if [ -z "$number" ]; then |
||||
echo "Input cannot be empty. Please enter a number." |
||||
elif ! [[ $number =~ ^[0-9]+$ ]]; then |
||||
echo "Invalid input. Please enter a valid number." |
||||
else |
||||
echo "$number" |
||||
return |
||||
fi |
||||
done |
||||
} |
||||
|
||||
read_number_range() { |
||||
local prompt="$1" |
||||
local min="$2" |
||||
local max="$3" |
||||
local default="$4" |
||||
local number |
||||
|
||||
while true; do |
||||
if [ -n "$default" ]; then |
||||
number=$(read_number "$prompt ($min-$max)" "$default") |
||||
else |
||||
number=$(read_number "$prompt ($min-$max)") |
||||
fi |
||||
|
||||
if [ -z "$number" ]; then |
||||
continue |
||||
elif [ "$number" -lt "$min" ] || [ "$number" -gt "$max" ]; then |
||||
echo "Please enter a number between $min and $max." |
||||
else |
||||
echo "$number" |
||||
return |
||||
fi |
||||
done |
||||
} |
||||
|
||||
check_if_docker_is_running() { |
||||
if ! $CONFIG_DOCKER_COMMAND ps >/dev/null 2>&1; then |
||||
print_warning "Docker is not running. Most of the commands will not work without Docker." |
||||
print_info "Use the following command to start Docker:" |
||||
print_color "$BLUE" " sudo systemctl start docker" |
||||
fi |
||||
} |
||||
|
||||
# Main functions |
||||
check_existing_installation() { |
||||
NOCO_FOUND=false |
||||
|
||||
# Check if $NOCO_HOME exists as directory |
||||
if [ -d "$NOCO_HOME" ]; then |
||||
NOCO_FOUND=true |
||||
elif $CONFIG_DOCKER_COMMAND ps --format '{{.Names}}' | grep -q "nocodb"; then |
||||
NOCO_ID=$($CONFIG_DOCKER_COMMAND ps | grep "nocodb/nocodb" | cut -d ' ' -f 1) |
||||
CUSTOM_HOME=$($CONFIG_DOCKER_COMMAND inspect --format='{{index .Mounts 0}}' "$NOCO_ID" | cut -d ' ' -f 3) |
||||
PARENT_DIR=$(dirname "$CUSTOM_HOME") |
||||
|
||||
ln -s "$PARENT_DIR" "$NOCO_HOME" |
||||
basename "$PARENT_DIR" > "$NOCO_HOME/.COMPOSE_PROJECT_NAME" |
||||
|
||||
NOCO_FOUND=true |
||||
else |
||||
mkdir -p "$NOCO_HOME" |
||||
fi |
||||
|
||||
cd "$NOCO_HOME" || exit 1 |
||||
|
||||
# Check if nocodb is already installed |
||||
if [ "$NOCO_FOUND" = true ]; then |
||||
echo "NocoDB is already installed. And running." |
||||
echo "Do you want to reinstall NocoDB? [Y/N] (default: N): " |
||||
read -r REINSTALL |
||||
|
||||
if [ -f "$NOCO_HOME/.COMPOSE_PROJECT_NAME" ]; then |
||||
COMPOSE_PROJECT_NAME=$(cat "$NOCO_HOME/.COMPOSE_PROJECT_NAME") |
||||
export COMPOSE_PROJECT_NAME |
||||
fi |
||||
|
||||
if [ "$REINSTALL" != "Y" ] && [ "$REINSTALL" != "y" ]; then |
||||
management_menu |
||||
exit 0 |
||||
else |
||||
echo "Reinstalling NocoDB..." |
||||
$CONFIG_DOCKER_COMMAND compose down |
||||
|
||||
unset COMPOSE_PROJECT_NAME |
||||
cd /tmp || exit 1 |
||||
rm -rf "$NOCO_HOME" |
||||
|
||||
cd "$CURRENT_PATH" || exit 1 |
||||
mkdir -p "$NOCO_HOME" |
||||
cd "$NOCO_HOME" || exit 1 |
||||
fi |
||||
fi |
||||
} |
||||
|
||||
check_system_requirements() { |
||||
print_info "Performing NocoDB system check and setup" |
||||
|
||||
for tool in docker wget lsof openssl; do |
||||
if ! command_exists "$tool"; then |
||||
print_warning "$tool is not installed. Setting up for installation..." |
||||
if [ "$tool" = "docker" ]; then |
||||
wget -qO- https://get.docker.com/ | sh |
||||
else |
||||
install_package "$tool" |
||||
fi |
||||
fi |
||||
done |
||||
|
||||
for port in "${REQUIRED_PORTS[@]}"; do |
||||
if lsof -Pi :"$port" -sTCP:LISTEN -t >/dev/null; then |
||||
print_warning "Port $port is in use. Please make sure it is free." |
||||
else |
||||
print_info "Port $port is free." |
||||
fi |
||||
done |
||||
|
||||
print_success "System check completed successfully" |
||||
} |
||||
|
||||
get_user_inputs() { |
||||
CONFIG_DOMAIN_NAME=$(prompt "Enter the IP address or domain name for the NocoDB instance" "$(get_public_ip)") |
||||
|
||||
if is_valid_domain "$CONFIG_DOMAIN_NAME"; then |
||||
if confirm "Do you want to configure SSL for $CONFIG_DOMAIN_NAME?"; then |
||||
CONFIG_SSL_ENABLED="Y" |
||||
else |
||||
CONFIG_SSL_ENABLED="N" |
||||
fi |
||||
else |
||||
CONFIG_SSL_ENABLED="N" |
||||
fi |
||||
|
||||
if confirm "Show Advanced Options?"; then |
||||
get_advanced_options |
||||
else |
||||
set_default_options |
||||
fi |
||||
} |
||||
|
||||
get_advanced_options() { |
||||
CONFIG_EDITION=$(prompt "Choose Community or Enterprise Edition [CE/EE]" "CE") |
||||
|
||||
if [ "$CONFIG_EDITION" = "EE" ] || [ "$CONFIG_EDITION" = "ee" ]; then |
||||
CONFIG_LICENSE_KEY=$(prompt_required "Enter the NocoDB license key") |
||||
fi |
||||
|
||||
CONFIG_REDIS_ENABLED=$(confirm "Do you want to enable Redis for caching?" "Y" && echo "Y" || echo "N" "Y") |
||||
CONFIG_MINIO_ENABLED=$(confirm "Do you want to enable Minio for file storage?" "Y" && echo "Y" || echo "N" "Y") |
||||
|
||||
if [ "$CONFIG_MINIO_ENABLED" = "Y" ] || [ "$CONFIG_MINIO_ENABLED" = "y" ]; then |
||||
|
||||
CONFIG_MINIO_DOMAIN_NAME=$(prompt "Enter the MinIO domain name" "$(get_public_ip)") |
||||
|
||||
if is_valid_domain "$CONFIG_MINIO_DOMAIN_NAME"; then |
||||
if confirm "Do you want to configure SSL for $CONFIG_MINIO_DOMAIN_NAME?"; then |
||||
CONFIG_MINIO_SSL_ENABLED="Y" |
||||
else |
||||
CONFIG_MINIO_SSL_ENABLED="N" |
||||
fi |
||||
else |
||||
CONFIG_MINIO_SSL_ENABLED="N" |
||||
fi |
||||
fi |
||||
|
||||
CONFIG_WATCHTOWER_ENABLED=$(confirm "Do you want to enable Watchtower for automatic updates?" "Y" && echo "Y" || echo "N") |
||||
|
||||
NUM_CORES=$(get_nproc) |
||||
CONFIG_NUM_INSTANCES=$(read_number_range "How many instances of NocoDB do you want to run?" 1 "$NUM_CORES" 1) |
||||
} |
||||
|
||||
set_default_options() { |
||||
CONFIG_SSL_ENABLED="N" |
||||
CONFIG_EDITION="CE" |
||||
CONFIG_REDIS_ENABLED="Y" |
||||
CONFIG_MINIO_ENABLED="Y" |
||||
CONFIG_MINIO_DOMAIN_NAME=$(get_public_ip) |
||||
CONFIG_MINIO_SSL_ENABLED="N" |
||||
CONFIG_WATCHTOWER_ENABLED="Y" |
||||
CONFIG_NUM_INSTANCES=1 |
||||
} |
||||
|
||||
generate_credentials() { |
||||
CONFIG_POSTGRES_PASSWORD=$(generate_password) |
||||
CONFIG_REDIS_PASSWORD=$(generate_password) |
||||
CONFIG_MINIO_ACCESS_KEY=$(generate_password) |
||||
CONFIG_MINIO_ACCESS_SECRET=$(generate_password) |
||||
} |
||||
|
||||
create_docker_compose_file() { |
||||
|
||||
image="nocodb/nocodb:latest" |
||||
|
||||
if [ "${CONFIG_EDITION}" = "EE" ] || [ "${CONFIG_EDITION}" = "ee" ]; then |
||||
image="nocodb/ee:latest" |
||||
fi |
||||
|
||||
local compose_file="docker-compose.yml" |
||||
|
||||
cat > "$compose_file" <<EOF |
||||
services: |
||||
nocodb: |
||||
image: ${image} |
||||
env_file: docker.env |
||||
deploy: |
||||
mode: replicated |
||||
replicas: ${CONFIG_NUM_INSTANCES} |
||||
depends_on: |
||||
- db |
||||
${CONFIG_REDIS_ENABLED:+- redis} |
||||
${CONFIG_MINIO_ENABLED:+- minio} |
||||
restart: unless-stopped |
||||
volumes: |
||||
- ./nocodb:/usr/app/data |
||||
labels: |
||||
- "com.centurylinklabs.watchtower.enable=true" |
||||
- "traefik.enable=true" |
||||
- "traefik.http.routers.nocodb.rule=Host(\`${CONFIG_DOMAIN_NAME}\`)" |
||||
EOF |
||||
# IF SSL is Enabled add the following lines |
||||
if [ "$CONFIG_SSL_ENABLED" = "Y" ]; then |
||||
cat >> "$compose_file" <<EOF |
||||
|
||||
- "traefik.http.routers.nocodb.entrypoints=websecure" |
||||
- "traefik.http.routers.nocodb.tls=true" |
||||
- "traefik.http.routers.nocodb.tls.certresolver=letsencrypt" |
||||
EOF |
||||
# If no ssl just configure the web entrypoint |
||||
else |
||||
cat >> "$compose_file" <<EOF |
||||
- "traefik.http.routers.nocodb.entrypoints=web" |
||||
EOF |
||||
fi |
||||
# Continue with the compose file |
||||
cat >> "$compose_file" <<EOF |
||||
networks: |
||||
- nocodb-network |
||||
|
||||
db: |
||||
image: postgres:16.1 |
||||
env_file: docker.env |
||||
volumes: |
||||
- ./postgres:/var/lib/postgresql/data |
||||
restart: unless-stopped |
||||
healthcheck: |
||||
test: ["CMD-SHELL", "pg_isready -U postgres"] |
||||
interval: 10s |
||||
timeout: 5s |
||||
retries: 5 |
||||
networks: |
||||
- nocodb-network |
||||
|
||||
traefik: |
||||
image: traefik:v3.1 |
||||
command: |
||||
- "--api.insecure=true" |
||||
- "--providers.docker=true" |
||||
- "--entrypoints.web.address=:80" |
||||
- "--providers.docker.exposedByDefault=false" |
||||
EOF |
||||
# In Traefik we need to add the minio entrypoint if it is enabled |
||||
if [ "$CONFIG_MINIO_ENABLED" = "Y" ]; then |
||||
cat >> "$compose_file" <<EOF |
||||
- "--entrypoints.minio.address=:9000" |
||||
EOF |
||||
fi |
||||
|
||||
# If SSL is enabled we need to add the following lines to the traefik service |
||||
if [ "$CONFIG_SSL_ENABLED" = "Y" ] || [ "$CONFIG_MINIO_SSL_ENABLED" = "Y" ]; then |
||||
cat >> "$compose_file" <<EOF |
||||
- "--entrypoints.websecure.address=:443" |
||||
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true" |
||||
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" |
||||
- "--certificatesresolvers.letsencrypt.acme.email=$(generate_contact_email $CONFIG_DOMAIN_NAME)" |
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/etc/letsencrypt/acme.json" |
||||
EOF |
||||
fi |
||||
# Continue with the compose file |
||||
cat >> "$compose_file" <<EOF |
||||
ports: |
||||
- "80:80" |
||||
- "443:443" |
||||
- "9000:9000" |
||||
depends_on: |
||||
- nocodb |
||||
restart: unless-stopped |
||||
volumes: |
||||
- "/var/run/docker.sock:/var/run/docker.sock" |
||||
- ./letsencrypt:/etc/letsencrypt |
||||
networks: |
||||
- nocodb-network |
||||
|
||||
EOF |
||||
|
||||
# If Redis is enabled add the following lines to the compose file |
||||
if [ "${CONFIG_REDIS_ENABLED}" = "Y" ]; then |
||||
cat >> "$compose_file" <<EOF |
||||
redis: |
||||
image: redis:latest |
||||
restart: unless-stopped |
||||
env_file: docker.env |
||||
command: |
||||
- /bin/sh |
||||
- -c |
||||
- redis-server --requirepass "\$\${REDIS_PASSWORD}" |
||||
healthcheck: |
||||
test: [ "CMD", "redis-cli", "-a", "\$\${REDIS_PASSWORD}", "--raw", "incr", "ping" ] |
||||
volumes: |
||||
- ./redis:/data |
||||
networks: |
||||
- nocodb-network |
||||
EOF |
||||
fi |
||||
|
||||
# IF Minio is enabled add the following lines to the compose file |
||||
if [ "${CONFIG_MINIO_ENABLED}" = "Y" ]; then |
||||
cat >> "$compose_file" <<EOF |
||||
minio: |
||||
image: minio/minio:latest |
||||
restart: unless-stopped |
||||
env_file: docker.env |
||||
entrypoint: /bin/sh |
||||
volumes: |
||||
- ./minio:/export |
||||
command: -c 'mkdir -p /export/nocodb && /usr/bin/minio server /export' |
||||
labels: |
||||
- "traefik.enable=true" |
||||
- "traefik.http.services.minio.loadbalancer.server.port=9000" |
||||
- "traefik.http.routers.minio.rule=Host(\`${CONFIG_MINIO_DOMAIN_NAME}\`)" |
||||
EOF |
||||
# If minio SSL is enabled, set the entry point to websecure |
||||
fi |
||||
if [ "$CONFIG_MINIO_SSL_ENABLED" = "Y" ]; then |
||||
cat >> "$compose_file" <<EOF |
||||
- "traefik.http.routers.minio.entrypoints=websecure" |
||||
- "traefik.http.routers.minio.tls=true" |
||||
- "traefik.http.routers.minio.tls.certresolver=letsencrypt" |
||||
EOF |
||||
# If minio is enabled and the domain is valid, set the entry point to web |
||||
elif is_valid_domain "$CONFIG_MINIO_DOMAIN_NAME"; then |
||||
cat >> "$compose_file" <<EOF |
||||
- "traefik.http.routers.minio.entrypoints=web" |
||||
EOF |
||||
|
||||
# If minio is enabled, valid domain name is not configured, set the entry point to Port 9000 |
||||
else |
||||
cat >> "$compose_file" <<EOF |
||||
- "traefik.http.routers.minio.entrypoints=minio" |
||||
EOF |
||||
fi |
||||
cat >> "$compose_file" <<EOF |
||||
networks: |
||||
- nocodb-network |
||||
|
||||
EOF |
||||
if [ "${CONFIG_WATCHTOWER_ENABLED}" = "Y" ]; then |
||||
cat >> "$compose_file" <<EOF |
||||
watchtower: |
||||
image: containrrr/watchtower |
||||
volumes: |
||||
- /var/run/docker.sock:/var/run/docker.sock |
||||
command: --schedule "0 2 * * 6" --cleanup |
||||
restart: unless-stopped |
||||
networks: |
||||
- nocodb-network |
||||
|
||||
EOF |
||||
fi |
||||
|
||||
cat >> "$compose_file" <<EOF |
||||
volumes: |
||||
${CONFIG_REDIS_ENABLED:+redis:} |
||||
|
||||
networks: |
||||
nocodb-network: |
||||
driver: bridge |
||||
EOF |
||||
} |
||||
|
||||
create_env_file() { |
||||
local env_file="docker.env" |
||||
local encoded_password |
||||
encoded_password=$(urlencode "${CONFIG_POSTGRES_PASSWORD}") |
||||
ENCODED_REDIS_PASSWORD=$(urlencode "$CONFIG_REDIS_PASSWORD") |
||||
cat > "$env_file" <<EOF |
||||
POSTGRES_DB=nocodb |
||||
POSTGRES_USER=postgres |
||||
POSTGRES_PASSWORD=${CONFIG_POSTGRES_PASSWORD} |
||||
EOF |
||||
|
||||
if [ "${CONFIG_EDITION}" = "EE" ] || [ "${CONFIG_EDITION}" = "ee" ]; then |
||||
echo "DATABASE_URL=postgres://postgres:${encoded_password}@db:5432/nocodb" >> "$env_file" |
||||
echo "NC_LICENSE_KEY=${CONFIG_LICENSE_KEY}" >> "$env_file" |
||||
else |
||||
echo "NC_DB=pg://db:5432?d=nocodb&user=postgres&password=${encoded_password}" >> "$env_file" |
||||
fi |
||||
|
||||
if [ "${CONFIG_REDIS_ENABLED}" = "Y" ]; then |
||||
cat >> "$env_file" <<EOF |
||||
REDIS_PASSWORD=${ENCODED_REDIS_PASSWORD} |
||||
NC_REDIS_URL=redis://:${ENCODED_REDIS_PASSWORD}@redis:6379/0 |
||||
EOF |
||||
fi |
||||
|
||||
if [ "${CONFIG_MINIO_ENABLED}" = "Y" ]; then |
||||
cat >> "$env_file" <<EOF |
||||
MINIO_ROOT_USER=${CONFIG_MINIO_ACCESS_KEY} |
||||
MINIO_ROOT_PASSWORD=${CONFIG_MINIO_ACCESS_SECRET} |
||||
NC_S3_BUCKET_NAME=nocodb |
||||
NC_S3_REGION=us-east-1 |
||||
NC_S3_ACCESS_KEY=${CONFIG_MINIO_ACCESS_KEY} |
||||
NC_S3_ACCESS_SECRET=${CONFIG_MINIO_ACCESS_SECRET} |
||||
NC_S3_FORCE_PATH_STYLE=true |
||||
EOF |
||||
if [ "$CONFIG_MINIO_SSL_ENABLED" = "Y" ]; then |
||||
echo "NC_S3_ENDPOINT=https://${CONFIG_MINIO_DOMAIN_NAME}" >> "$env_file" |
||||
elif is_valid_domain "$CONFIG_MINIO_DOMAIN_NAME"; then |
||||
echo "NC_S3_ENDPOINT=http://${CONFIG_MINIO_DOMAIN_NAME}" >> "$env_file" |
||||
else |
||||
echo "NC_S3_ENDPOINT=http://${CONFIG_MINIO_DOMAIN_NAME}:9000" >> "$env_file" |
||||
fi |
||||
fi |
||||
} |
||||
|
||||
create_update_script() { |
||||
cat > ./update.sh <<EOF |
||||
#!/bin/bash |
||||
$CONFIG_DOCKER_COMMAND compose pull |
||||
$CONFIG_DOCKER_COMMAND compose up -d --force-recreate |
||||
$CONFIG_DOCKER_COMMAND image prune -a -f |
||||
EOF |
||||
chmod +x ./update.sh |
||||
message_arr+=("Update script: update.sh") |
||||
} |
||||
|
||||
start_services() { |
||||
$CONFIG_DOCKER_COMMAND compose pull |
||||
$CONFIG_DOCKER_COMMAND compose up -d |
||||
|
||||
echo 'Waiting for Traefik to start...' |
||||
sleep 5 |
||||
} |
||||
|
||||
display_completion_message() { |
||||
if [ -n "${CONFIG_DOMAIN_NAME}" ]; then |
||||
if [ "${CONFIG_SSL_ENABLED}" = "Y" ]; then |
||||
message_arr+=("NocoDB is now available at https://${CONFIG_DOMAIN_NAME}") |
||||
else |
||||
message_arr+=("NocoDB is now available at http://${CONFIG_DOMAIN_NAME}") |
||||
fi |
||||
else |
||||
message_arr+=("NocoDB is now available at http://localhost") |
||||
fi |
||||
|
||||
print_box_message "${message_arr[@]}" |
||||
} |
||||
|
||||
management_menu() { |
||||
while true; do |
||||
trap - INT |
||||
show_menu |
||||
echo "Enter your choice: " |
||||
|
||||
read -r choice |
||||
case $choice in |
||||
1) start_service && MSG="NocoDB Started" ;; |
||||
2) stop_service && MSG="NocoDB Stopped" ;; |
||||
3) show_logs ;; |
||||
4) restart_service && MSG="NocoDB Restarted" ;; |
||||
5) upgrade_service && MSG="NocoDB has been upgraded to latest version" ;; |
||||
6) scale_service && MSG="NocoDB has been scaled" ;; |
||||
7) monitoring_service ;; |
||||
0) exit 0 ;; |
||||
*) MSG="\nInvalid choice. Please select a correct option." ;; |
||||
esac |
||||
done |
||||
} |
||||
|
||||
show_menu() { |
||||
clear |
||||
check_if_docker_is_running |
||||
echo "" |
||||
echo "$MSG" |
||||
echo -e "\t\t${BOLD}Service Management Menu${NC}" |
||||
echo -e " ${GREEN}1. Start Service" |
||||
echo -e " ${ORANGE}2. Stop Service" |
||||
echo -e " ${CYAN}3. Logs" |
||||
echo -e " ${MAGENTA}4. Restart" |
||||
echo -e " ${BLUE}5. Upgrade" |
||||
echo -e " 6. Scale" |
||||
echo -e " 7. Monitoring" |
||||
echo -e " ${RED}0. Exit${NC}" |
||||
} |
||||
|
||||
start_service() { |
||||
echo -e "\nStarting nocodb..." |
||||
$CONFIG_DOCKER_COMMAND compose up -d |
||||
} |
||||
|
||||
stop_service() { |
||||
echo -e "\nStopping nocodb..." |
||||
$CONFIG_DOCKER_COMMAND compose stop |
||||
} |
||||
|
||||
show_logs_sub_menu() { |
||||
clear |
||||
echo "Select a replica for $1:" |
||||
for i in $(seq 1 $2); do |
||||
echo "$i. \"$1\" replica $i" |
||||
done |
||||
echo "A. All" |
||||
echo "0. Back to Logs Menu" |
||||
echo "Enter replica number: " |
||||
read -r replica_choice |
||||
|
||||
if [[ "$replica_choice" =~ ^[0-9]+$ ]] && [ "$replica_choice" -gt 0 ] && [ "$replica_choice" -le "$2" ]; then |
||||
container_id=$($CONFIG_DOCKER_COMMAND compose ps | grep "$1-$replica_choice" | cut -d " " -f 1) |
||||
$CONFIG_DOCKER_COMMAND logs -f "$container_id" |
||||
elif [ "$replica_choice" == "A" ] || [ "$replica_choice" == "a" ]; then |
||||
$CONFIG_DOCKER_COMMAND compose logs -f "$1" |
||||
elif [ "$replica_choice" == "0" ]; then |
||||
show_logs |
||||
else |
||||
show_logs_sub_menu "$1" "$2" |
||||
fi |
||||
} |
||||
|
||||
# Function to show logs |
||||
show_logs() { |
||||
clear |
||||
echo "Select a container for logs:" |
||||
|
||||
# Fetch the list of services |
||||
services=() |
||||
while IFS= read -r service; do |
||||
services+=("$service") |
||||
done < <($CONFIG_DOCKER_COMMAND compose ps --services) |
||||
|
||||
service_replicas=() |
||||
count=0 |
||||
|
||||
# For each service, count the number of running instances |
||||
for service in "${services[@]}"; do |
||||
# Count the number of lines that have the service name, which corresponds to the number of replicas |
||||
replicas=$($CONFIG_DOCKER_COMMAND compose ps "$service" | grep -c "$service") |
||||
service_replicas["$count"]=$replicas |
||||
count=$((count + 1)) |
||||
done |
||||
|
||||
count=1 |
||||
|
||||
for service in "${services[@]}"; do |
||||
echo "$count. $service (${service_replicas[(($count - 1))]} replicas)" |
||||
count=$((count + 1)) |
||||
done |
||||
|
||||
echo "A. All" |
||||
echo "0. Back to main menu" |
||||
echo "Enter your choice: " |
||||
read -r log_choice |
||||
echo |
||||
|
||||
if [[ "$log_choice" =~ ^[0-9]+$ ]] && [ "$log_choice" -gt 0 ] && [ "$log_choice" -lt "$count" ]; then |
||||
service_index=$((log_choice-1)) |
||||
service="${services[$service_index]}" |
||||
num_replicas="${service_replicas[$service_index]}" |
||||
|
||||
if [ "$num_replicas" -gt 1 ]; then |
||||
trap 'show_logs_sub_menu "$service" "$num_replicas"' INT |
||||
show_logs_sub_menu "$service" "$num_replicas" |
||||
trap - INT |
||||
else |
||||
trap 'show_logs' INT |
||||
$CONFIG_DOCKER_COMMAND compose logs -f "$service" |
||||
fi |
||||
elif [ "$log_choice" == "A" ] || [ "$log_choice" == "a" ]; then |
||||
trap 'show_logs' INT |
||||
$CONFIG_DOCKER_COMMAND compose logs -f |
||||
elif [ "$log_choice" == "0" ]; then |
||||
return |
||||
else |
||||
show_logs |
||||
fi |
||||
|
||||
trap - INT |
||||
} |
||||
|
||||
restart_service() { |
||||
echo -e "\nRestarting nocodb..." |
||||
$CONFIG_DOCKER_COMMAND compose restart |
||||
} |
||||
|
||||
upgrade_service() { |
||||
echo -e "\nUpgrading nocodb..." |
||||
$CONFIG_DOCKER_COMMAND compose pull |
||||
$CONFIG_DOCKER_COMMAND compose up -d --force-recreate |
||||
$CONFIG_DOCKER_COMMAND image prune -a -f |
||||
} |
||||
|
||||
scale_service() { |
||||
num_cores=$(get_nproc) |
||||
current_scale=$($CONFIG_DOCKER_COMMAND compose ps -q nocodb | wc -l) |
||||
echo -e "\nCurrent number of instances: $current_scale" |
||||
scale_num=$(read_number_range "How many instances of NocoDB do you want to run?" 1 "$num_cores" 1) |
||||
|
||||
if [ "$scale_num" -eq "$current_scale" ]; then |
||||
echo "Number of instances is already set to $scale_num. Returning to main menu." |
||||
return |
||||
fi |
||||
|
||||
$CONFIG_DOCKER_COMMAND compose up -d --scale nocodb="$scale_num" |
||||
} |
||||
|
||||
monitoring_service() { |
||||
echo -e '\nLoading stats...' |
||||
trap ' ' INT |
||||
$CONFIG_DOCKER_COMMAND stats |
||||
} |
||||
|
||||
|
||||
main() { |
||||
CONFIG_DOCKER_COMMAND=$([ "$(check_for_docker_sudo)" = "y" ] && echo "sudo docker" || echo "docker") |
||||
|
||||
check_existing_installation |
||||
check_system_requirements |
||||
get_user_inputs |
||||
generate_credentials |
||||
create_docker_compose_file |
||||
add_to_hosts |
||||
create_env_file |
||||
create_update_script |
||||
start_services |
||||
display_completion_message |
||||
|
||||
if confirm "Do you want to start the management menu?"; then |
||||
management_menu |
||||
fi |
||||
} |
||||
|
||||
main "$@" |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 544 B |
After Width: | Height: | Size: 460 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 386 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 405 B After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 537 B |
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,66 @@
|
||||
<script setup lang="ts"> |
||||
const workspaceStore = useWorkspace() |
||||
|
||||
const { navigateToFeed } = workspaceStore |
||||
|
||||
const { isFeedPageOpened } = storeToRefs(workspaceStore) |
||||
|
||||
const { isNewFeedAvailable } = useProductFeed() |
||||
|
||||
const gotoFeed = () => navigateToFeed() |
||||
</script> |
||||
|
||||
<template> |
||||
<NcButton |
||||
v-e="['c:nocodb:feed']" |
||||
type="text" |
||||
full-width |
||||
size="xsmall" |
||||
class="n!xs:hidden my-0.5 w-full !h-7 !rounded-md !font-normal !pl-4.5 !pr-5" |
||||
data-testid="nc-sidebar-product-feed" |
||||
:centered="false" |
||||
:class="{ |
||||
'!text-brand-600 !bg-brand-50 !hover:bg-brand-50': isFeedPageOpened, |
||||
'!hover:(bg-gray-200 text-gray-700)': !isFeedPageOpened, |
||||
}" |
||||
@click="gotoFeed" |
||||
> |
||||
<div |
||||
class="flex !w-full items-center gap-2" |
||||
:class="{ |
||||
'font-semibold': isFeedPageOpened, |
||||
}" |
||||
> |
||||
<div class="flex flex-1 w-full items-center gap-3"> |
||||
<GeneralIcon icon="megaPhone" class="!h-4" /> |
||||
<span class="">What’s New!</span> |
||||
</div> |
||||
<div v-if="isNewFeedAvailable" class="w-3 h-3 pulsing-dot bg-nc-fill-red-medium border-2 border-white rounded-full"></div> |
||||
</div> |
||||
</NcButton> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
@keyframes pulse { |
||||
0% { |
||||
transform: scale(1); |
||||
opacity: 1; |
||||
} |
||||
50% { |
||||
transform: scale(1.1); |
||||
opacity: 0.7; |
||||
} |
||||
100% { |
||||
transform: scale(1); |
||||
opacity: 1; |
||||
} |
||||
} |
||||
|
||||
.pulsing-dot { |
||||
animation: pulse 1.5s infinite ease-in-out; |
||||
} |
||||
|
||||
:deep(.nc-btn-inner) { |
||||
@apply !w-full; |
||||
} |
||||
</style> |
@ -0,0 +1,185 @@
|
||||
<script lang="ts" setup> |
||||
/** |
||||
* ExtensionHeader component. |
||||
* |
||||
* @slot prefix - Slot for custom content to be displayed at the start of the header when in fullscreen mode. |
||||
* @slot extra - Slot for additional custom content to be displayed before the options and close button in fullscreen mode. |
||||
*/ |
||||
|
||||
interface Props { |
||||
isFullscreen?: boolean |
||||
} |
||||
|
||||
withDefaults(defineProps<Props>(), { |
||||
isFullscreen: true, |
||||
}) |
||||
|
||||
const { eventBus, getExtensionAssetsUrl, duplicateExtension, showExtensionDetails } = useExtensions() |
||||
|
||||
const { fullscreen, collapsed, extension, extensionManifest, activeError, showExpandBtn } = useExtensionHelperOrThrow() |
||||
|
||||
const titleInput = ref<HTMLInputElement | null>(null) |
||||
|
||||
const titleEditMode = ref<boolean>(false) |
||||
|
||||
const tempTitle = ref<string>(extension.value.title) |
||||
|
||||
const showExpandButton = computed(() => { |
||||
return showExpandBtn.value && !activeError.value |
||||
}) |
||||
|
||||
const enableEditMode = () => { |
||||
titleEditMode.value = true |
||||
tempTitle.value = extension.value.title |
||||
nextTick(() => { |
||||
titleInput.value?.focus() |
||||
titleInput.value?.select() |
||||
}) |
||||
} |
||||
|
||||
const updateExtensionTitle = async () => { |
||||
await extension.value.setTitle(tempTitle.value) |
||||
titleEditMode.value = false |
||||
} |
||||
|
||||
const expandExtension = () => { |
||||
if (!collapsed.value) return |
||||
|
||||
collapsed.value = false |
||||
} |
||||
/** |
||||
* Handles the duplication of an extension. |
||||
* |
||||
* @param id - The ID of the extension to duplicate. |
||||
* @param open - Optional. If true, the duplicated extension will be opened. |
||||
*/ |
||||
|
||||
const handleDuplicateExtension = async (id: string, open: boolean = false) => { |
||||
const duplicatedExt = await duplicateExtension(id) |
||||
|
||||
if (duplicatedExt?.id && open) { |
||||
fullscreen.value = false |
||||
eventBus.emit(ExtensionsEvents.DUPLICATE, duplicatedExt.id) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
v-if="(isFullscreen && fullscreen) || !isFullscreen" |
||||
class="extension-header flex items-center" |
||||
:class="{ |
||||
'border-b-1 border-nc-border-gray-medium h-[49px]': !collapsed && !isFullscreen, |
||||
'collapsed border-transparent h-[48px]': collapsed && !isFullscreen, |
||||
'px-3 py-2 gap-1': !isFullscreen, |
||||
'gap-3 px-4 pt-4 pb-[15px] border-b-1 border-nc-border-gray-medium': isFullscreen, |
||||
}" |
||||
@click="expandExtension" |
||||
> |
||||
<slot v-if="isFullscreen" name="prefix"></slot> |
||||
<NcButton v-if="!isFullscreen" size="xs" type="text" class="nc-extension-drag-handler !px-1" @click.stop> |
||||
<GeneralIcon icon="ncDrag" class="flex-none text-gray-500" /> |
||||
</NcButton> |
||||
|
||||
<img |
||||
v-if="extensionManifest" |
||||
:src="getExtensionAssetsUrl(extensionManifest.iconUrl)" |
||||
alt="icon" |
||||
class="h-8 w-8 object-contain flex-none" |
||||
:class="{ |
||||
'mx-1': !isFullscreen, |
||||
}" |
||||
/> |
||||
<div |
||||
v-if="titleEditMode" |
||||
class="flex-1" |
||||
:class="{ |
||||
'mr-1': !isFullscreen, |
||||
}" |
||||
> |
||||
<a-input |
||||
ref="titleInput" |
||||
v-model:value="tempTitle" |
||||
type="text" |
||||
class="!h-8 !px-1 !py-1 !-ml-1 !rounded-lg extension-title" |
||||
:class="{ |
||||
'w-4/5': !isFullscreen, |
||||
'!text-lg !font-semibold max-w-[420px]': isFullscreen, |
||||
}" |
||||
@click.stop |
||||
@keyup.enter="updateExtensionTitle" |
||||
@keyup.esc="updateExtensionTitle" |
||||
@blur="updateExtensionTitle" |
||||
> |
||||
</a-input> |
||||
</div> |
||||
|
||||
<NcTooltip v-else show-on-truncate-only class="truncate flex-1"> |
||||
<template #title> |
||||
{{ extension.title }} |
||||
</template> |
||||
<span |
||||
class="extension-title cursor-pointer" |
||||
:class="{ |
||||
'text-lg font-semibold ': isFullscreen, |
||||
'mr-1': !isFullscreen, |
||||
}" |
||||
@dblclick.stop="enableEditMode" |
||||
@click.stop |
||||
> |
||||
{{ extension.title }} |
||||
</span> |
||||
</NcTooltip> |
||||
<slot v-if="isFullscreen" name="extra"></slot> |
||||
<ExtensionsExtensionHeaderMenu |
||||
:is-fullscreen="isFullscreen" |
||||
class="nc-extension-menu" |
||||
@rename="enableEditMode" |
||||
@duplicate="handleDuplicateExtension(extension.id, true)" |
||||
@show-details="showExtensionDetails(extension.extensionId, 'extension')" |
||||
@clear-data="extension.clear()" |
||||
@delete="extension.delete()" |
||||
/> |
||||
|
||||
<template v-if="!isFullscreen"> |
||||
<NcButton |
||||
v-if="showExpandButton" |
||||
size="xs" |
||||
type="text" |
||||
class="nc-extension-expand-btn !px-1" |
||||
@click.stop="fullscreen = true" |
||||
> |
||||
<GeneralIcon icon="ncMaximize2" class="h-3.5 w-3.5" /> |
||||
</NcButton> |
||||
<NcButton size="xs" type="text" class="!px-1" @click.stop="collapsed = !collapsed"> |
||||
<GeneralIcon :icon="collapsed ? 'arrowDown' : 'arrowUp'" class="flex-none" /> |
||||
</NcButton> |
||||
</template> |
||||
<NcButton v-else :size="isFullscreen ? 'small' : 'xs'" type="text" class="flex-none !px-1" @click="fullscreen = false"> |
||||
<GeneralIcon icon="close" /> |
||||
</NcButton> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
.extension-header { |
||||
&.collapsed:not(:hover) { |
||||
.nc-extension-expand-btn, |
||||
.nc-extension-menu { |
||||
@apply hidden; |
||||
} |
||||
} |
||||
|
||||
.extension-header-left { |
||||
@apply flex-1 flex items-center gap-2; |
||||
} |
||||
|
||||
.extension-header-right { |
||||
@apply flex items-center gap-1; |
||||
} |
||||
|
||||
.extension-title { |
||||
@apply font-weight-600; |
||||
} |
||||
} |
||||
</style> |
@ -1,17 +1,19 @@
|
||||
<script setup lang="ts"> |
||||
interface Props { |
||||
fullscreen?: boolean |
||||
activeError?: boolean |
||||
isFullscreen?: boolean |
||||
} |
||||
|
||||
const { fullscreen, activeError } = defineProps<Props>() |
||||
defineProps<Props>() |
||||
|
||||
const emits = defineEmits(['rename', 'duplicate', 'showDetails', 'clearData', 'delete']) |
||||
|
||||
const { activeError } = useExtensionHelperOrThrow() |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex items-center"> |
||||
<div class="flex items-center" @click.stop> |
||||
<NcDropdown :trigger="['click']" placement="bottomRight"> |
||||
<NcButton type="text" :size="fullscreen ? 'small' : 'xs'" class="!px-1"> |
||||
<NcButton type="text" :size="isFullscreen ? 'small' : 'xs'" class="!px-1"> |
||||
<GeneralIcon icon="threeDotVertical" /> |
||||
</NcButton> |
||||
|