Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Web UI to support querying from browsers. #250

Merged
merged 64 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
a8fee24
Add Web UI to support querying from browser clients.
junhaoliao Jan 26, 2024
84bb372
Add docs.
junhaoliao Jan 26, 2024
d891d04
Change SearchFilterControlsDrawer background color to grey.
junhaoliao Jan 26, 2024
33342c6
Hide "Max lines per result" settings button when there is no result.
junhaoliao Jan 26, 2024
d89c727
Add log to file support with daily rotation.
junhaoliao Jan 27, 2024
af4572b
Support WebUI logging configuration in CLP package.
junhaoliao Jan 27, 2024
2364bce
Add WebUI launcher to redirect stderr of Meteor server to logs direct…
junhaoliao Jan 28, 2024
3b88853
In production mode also override winston logging function to allow lo…
junhaoliao Jan 28, 2024
d41a4eb
Add docs for logging utilities.
junhaoliao Jan 28, 2024
cd9604d
Add CDN for font "Source Sans Pro"
junhaoliao Jan 28, 2024
fda1d2f
Change search progress visualization to prevent elements moving in be…
junhaoliao Jan 29, 2024
5852329
Change default time range from "Last 15 Minutes" to "All".
junhaoliao Jan 29, 2024
5f5e5f0
Remove "Cancelling..." text from SearchStatus to prevent results elem…
junhaoliao Jan 29, 2024
67df5ea
Refine Search Control borders visuals; Re-focus on Search Input once …
junhaoliao Jan 29, 2024
d7773c1
Merge branch 'main' into main
junhaoliao Jan 29, 2024
96f3cc8
Update Taskfile `webui` build condition.
junhaoliao Feb 3, 2024
60d1744
Update clp/docs/Building.md to include WebUI requirements; Update web…
junhaoliao Feb 3, 2024
0186a6c
Refactor datetime utilities; Replace MY_MONGO_DB object with Map; Ref…
junhaoliao Feb 3, 2024
0933d15
Merge branch 'main' into main
junhaoliao Feb 3, 2024
8969abf
Update .yamllint.yml to exclude `build/` and `webui/node_modules`; Up…
junhaoliao Feb 3, 2024
6693fa3
Fix search jobs table name; Use column name constants; Remove unused …
kirkrodrigues Feb 4, 2024
8f7b48a
Refactor datetime.js
kirkrodrigues Feb 4, 2024
f6a093c
Refactor docs.
kirkrodrigues Feb 4, 2024
fc8204c
Manually format webui startup command.
kirkrodrigues Feb 5, 2024
67482d0
Merge branch 'main' into main
kirkrodrigues Feb 5, 2024
08487b1
Split SearchView styles into file per sub-component.
kirkrodrigues Feb 5, 2024
a21ae8e
Remove unnecessary styles.
kirkrodrigues Feb 5, 2024
ced0114
Rename some files.
kirkrodrigues Feb 5, 2024
6145aa9
End all files with a blank line.
kirkrodrigues Feb 5, 2024
c24244f
Fix some docstrings.
kirkrodrigues Feb 5, 2024
f4836a5
Remove unused RESP_ERROR constant.
kirkrodrigues Feb 5, 2024
b585e56
Pass database host, port, and name through settings.json along with t…
kirkrodrigues Feb 6, 2024
5628e03
Remove unused highcharts dependency.
kirkrodrigues Feb 6, 2024
db259df
Add new settings missed in previous commit.
kirkrodrigues Feb 7, 2024
a9ee0a3
Replace MongoDB collections map with a class for more robust management.
kirkrodrigues Feb 7, 2024
87b1be4
Use correct case for logging level.
kirkrodrigues Feb 7, 2024
82a309d
Sort search results on both the client and server.
kirkrodrigues Feb 7, 2024
5ccc8a7
Add docs for webui/launcher.js
junhaoliao Feb 7, 2024
d191d53
Replace sql.js with SearchJobsDbManager class for more robust managem…
kirkrodrigues Feb 7, 2024
bc4665b
Minor error handling improvements.
kirkrodrigues Feb 7, 2024
0ed9433
Minor docstring fix.
kirkrodrigues Feb 7, 2024
bb9a67d
Fix reference to SearchJobStatus enum.
kirkrodrigues Feb 7, 2024
21cd52a
Refactor new tasks.
kirkrodrigues Feb 7, 2024
4a78012
Use correct path for webui build dir.
kirkrodrigues Feb 7, 2024
393c1d7
Reorder new tasks.
kirkrodrigues Feb 7, 2024
0000683
Merge branch 'kirkrodrigues:webui' into jl-main
kirkrodrigues Feb 7, 2024
7e3a6de
Update packages.
kirkrodrigues Feb 7, 2024
df01906
Add IngestionView to display compression stats.
junhaoliao Feb 7, 2024
c3c23c1
Remove unused styles.
kirkrodrigues Feb 8, 2024
a6dd9ca
Move compression stats periodic refreshing logics from every subscrip…
junhaoliao Feb 8, 2024
65e3a08
Add jsdoc as a dev dependency and enable documentation generation.
junhaoliao Feb 8, 2024
9f5bea3
Rename SqlDbArchivesTableName to SqlDbClpArchivesTableName for consis…
kirkrodrigues Feb 8, 2024
926ad68
Apply naming conventions to some variables..
kirkrodrigues Feb 8, 2024
1e58b1f
Support big numbers from MySQL; Cast stats to numbers before rendering.
kirkrodrigues Feb 8, 2024
c0fcb78
Allow stats to be null.
kirkrodrigues Feb 8, 2024
59263cd
Minor clean-up.
kirkrodrigues Feb 8, 2024
ee0b736
Pass clp table names from startup script.
kirkrodrigues Feb 8, 2024
a1f8dde
Fix broken log.
kirkrodrigues Feb 8, 2024
c3eae99
Minor refactoring.
kirkrodrigues Feb 8, 2024
ce7d7cf
Minor refactoring and docstring fix; Replace path to NodeJS binary wi…
junhaoliao Feb 9, 2024
dada28f
Correct webui rebuild condition in Taskfile.
junhaoliao Feb 9, 2024
dd4f5ef
Minor refactoring.
junhaoliao Feb 9, 2024
d9897cb
Minor refactoring.
junhaoliao Feb 9, 2024
90e1480
Minor refactoring.
junhaoliao Feb 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ version: "3"
vars:
BUILD_DIR: "{{.TASKFILE_DIR}}/build"
CORE_COMPONENT_BUILD_DIR: "{{.TASKFILE_DIR}}/build/core"
WEBUI_BUILD_DIR: "{{.TASKFILE_DIR}}/build/webui"
NODEJS_BUILD_DIR: "{{.TASKFILE_DIR}}/build/nodejs"
NODEJS_BIN_DIR: "{{.NODEJS_BUILD_DIR}}/node/bin"
LINT_VENV_DIR: "{{.TASKFILE_DIR}}/.lint-venv"
PACKAGE_BUILD_DIR: "{{.TASKFILE_DIR}}/build/clp-package"
PACKAGE_VENV_DIR: "{{.TASKFILE_DIR}}/build/package-venv"
Expand All @@ -29,7 +32,6 @@ tasks:
- task: "clean-python-component"
vars:
COMPONENT: "job-orchestration"

kirkrodrigues marked this conversation as resolved.
Show resolved Hide resolved
clean-package:
cmds:
- "rm -rf '{{.PACKAGE_BUILD_DIR}}'"
Expand Down Expand Up @@ -64,6 +66,7 @@ tasks:
- "compression-job-handler"
- "job-orchestration"
- "package-venv"
- "webui"
cmds:
- task: "clean-package"
- "mkdir -p '{{.PACKAGE_BUILD_DIR}}'"
Expand All @@ -83,7 +86,13 @@ tasks:
"{{.CORE_COMPONENT_BUILD_DIR}}/clg"
"{{.CORE_COMPONENT_BUILD_DIR}}/clo"
"{{.CORE_COMPONENT_BUILD_DIR}}/clp"
"{{.BUILD_DIR}}/nodejs/node/bin/node"
kirkrodrigues marked this conversation as resolved.
Show resolved Hide resolved
"{{.PACKAGE_BUILD_DIR}}/bin/"
- "mkdir -p '{{.PACKAGE_BUILD_DIR}}/var/www/'"
- >-
rsync -a --delete
"{{.WEBUI_BUILD_DIR}}/"
"{{.PACKAGE_BUILD_DIR}}/var/www/"
# This step must be last since we use this file to detect whether the package was built
# successfully
- "echo {{.PACKAGE_VERSION}} > '{{.PACKAGE_VERSION_FILE}}'"
Expand All @@ -92,6 +101,7 @@ tasks:
- "{{.CORE_COMPONENT_BUILD_DIR}}/clg"
- "{{.CORE_COMPONENT_BUILD_DIR}}/clo"
- "{{.CORE_COMPONENT_BUILD_DIR}}/clp"
- "{{.BUILD_DIR}}/webui/built/**/*"
- "{{.TASKFILE_DIR}}/Taskfile.yml"
- "components/clp-package-utils/dist/*.whl"
- "components/clp-py-utils/dist/*.whl"
Expand All @@ -102,6 +112,52 @@ tasks:
- "test -e '{{.PACKAGE_VERSION_FILE}}'"
- "test {{.TIMESTAMP | unixEpoch}} -lt $(stat --format %Y '{{.PACKAGE_VERSION_FILE}}')"

webui:
deps:
- "nodejs"
dir: "components/webui"
cmds:
- "rm -rf '{{.WEBUI_BUILD_DIR}}'"
- "mkdir -p {{.WEBUI_BUILD_DIR}}"
- "meteor npm install --production"
- "meteor build --directory {{.WEBUI_BUILD_DIR}}"
- >-
rsync -a
"{{.WEBUI_BUILD_DIR}}/bundle/"
"$PWD/settings.json"
"$PWD/launcher.js"
"{{.WEBUI_BUILD_DIR}}/"
- "rm -rf '{{.WEBUI_BUILD_DIR}}/bundle/'"
- |-
cd {{.WEBUI_BUILD_DIR}}/programs/server
PATH={{.NODEJS_BIN_DIR}}:$PATH $(readlink -f {{.NODEJS_BIN_DIR}}/npm) install
sources:
- "./.meteor/*"
- "./client/**/*"
- "./imports/**/*"
- "./server/**/*"
- "./tests/**/*"
- "./*"

nodejs:
vars:
NODEJS_VERSION: "14.21.3"
NODEJS_RELEASE: "v{{.NODEJS_VERSION}}-linux-x64"
TAR_FILENAME: "node-{{.NODEJS_RELEASE}}.tar.xz"
TAR_FILE_PATH: "{{.NODEJS_BUILD_DIR}}/{{.TAR_FILENAME}}"
cmds:
- "rm -rf '{{.NODEJS_BUILD_DIR}}/node'"
- "mkdir -p {{.NODEJS_BUILD_DIR}}"
- >-
curl -fsSL
https://nodejs.org/dist/v{{.NODEJS_VERSION}}/{{.TAR_FILENAME}}
-o {{.TAR_FILE_PATH}}
- "tar xf {{.TAR_FILE_PATH}} -C {{.NODEJS_BUILD_DIR}}"
- "mv {{.NODEJS_BUILD_DIR}}/node-{{.NODEJS_RELEASE}} {{.NODEJS_BUILD_DIR}}/node"
- "rm -f '{{.TAR_FILE_PATH}}'"
sources:
kirkrodrigues marked this conversation as resolved.
Show resolved Hide resolved
- "{{.NODEJS_BUILD_DIR}}/node/**/*"

core:
deps: ["core-submodules"]
vars:
Expand Down
20 changes: 18 additions & 2 deletions components/clp-package-utils/clp_package_utils/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
CLP_DEFAULT_CREDENTIALS_FILE_PATH,
DB_COMPONENT_NAME,
QUEUE_COMPONENT_NAME,
RESULTS_CACHE_COMPONENT_NAME
RESULTS_CACHE_COMPONENT_NAME,
WEBUI_COMPONENT_NAME,
)
from clp_py_utils.core import (
get_config_value,
make_config_path_absolute,
read_yaml_config_file,
validate_path_could_be_dir
validate_path_could_be_dir,
)

# CONSTANTS
Expand Down Expand Up @@ -60,6 +61,7 @@ def __init__(self, clp_home: pathlib.Path, docker_clp_home: pathlib.Path):
self.logs_dir: typing.Optional[DockerMount] = None
self.archives_output_dir: typing.Optional[DockerMount] = None


def get_clp_home():
# Determine CLP_HOME from an environment variable or this script's path
clp_home = None
Expand All @@ -78,6 +80,7 @@ def get_clp_home():

return clp_home.resolve()


def check_dependencies():
try:
subprocess.run("command -v docker", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True)
Expand Down Expand Up @@ -277,3 +280,16 @@ def validate_results_cache_config(clp_config: CLPConfig, data_dir: pathlib.Path,
def validate_worker_config(clp_config: CLPConfig):
clp_config.validate_input_logs_dir()
clp_config.validate_archive_output_dir()


def validate_webui_config(clp_config: CLPConfig, logs_dir: pathlib.Path, settings_json_path: pathlib.Path):
if not settings_json_path.exists():
raise ValueError(f"{WEBUI_COMPONENT_NAME} {settings_json_path} is not a valid path to Meteor settings.json")

try:
validate_path_could_be_dir(logs_dir)
except ValueError as ex:
raise ValueError(f"{WEBUI_COMPONENT_NAME} logs directory is invalid: {ex}")

validate_port(f"{WEBUI_COMPONENT_NAME}.port", clp_config.webui.host,
clp_config.webui.port)
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import argparse
import json
import logging
import multiprocessing
import os
Expand Down Expand Up @@ -29,6 +30,7 @@
validate_db_config,
validate_queue_config,
validate_results_cache_config,
validate_webui_config,
validate_worker_config
)
from clp_py_utils.clp_config import (
Expand All @@ -40,6 +42,7 @@
SEARCH_SCHEDULER_COMPONENT_NAME,
SEARCH_WORKER_COMPONENT_NAME,
WORKER_COMPONENT_NAME,
WEBUI_COMPONENT_NAME,
)
from job_orchestration.scheduler.constants import QueueName

Expand Down Expand Up @@ -544,6 +547,70 @@ def start_worker(instance_id: str, clp_config: CLPConfig, container_clp_config:
logger.info(f"Started {WORKER_COMPONENT_NAME}.")


def start_webui(instance_id: str, clp_config: CLPConfig, mounts: CLPDockerMounts):
logger.info(f"Starting {WEBUI_COMPONENT_NAME}...")

container_name = f'clp-{WEBUI_COMPONENT_NAME}-{instance_id}'
if container_exists(container_name):
logger.info(f"{WEBUI_COMPONENT_NAME} already running.")
return

webui_logs_dir = clp_config.logs_directory / WEBUI_COMPONENT_NAME
node_path = str(CONTAINER_CLP_HOME / 'var' / 'www' / 'programs'/'server'/'npm'/'node_modules')
settings_json_path = get_clp_home() / 'var' / 'www' / 'settings.json'

validate_webui_config(clp_config, webui_logs_dir, settings_json_path)

# Create directories
webui_logs_dir.mkdir(exist_ok=True, parents=True)

container_webui_logs_dir = pathlib.Path('/') / 'var' / 'log' / WEBUI_COMPONENT_NAME
with open(settings_json_path, 'r') as settings_json_file:
settings_json_content = settings_json_file.read()
meteor_settings = json.loads(settings_json_content)

# Start container
container_cmd = [
'docker', 'run',
'-d',
'--network', 'host',
'--rm',
'--name', container_name,
'-e', f"NODE_PATH={node_path}",
'-e', f'MONGO_URL={clp_config.results_cache.get_uri()}',
'-e', f'PORT={clp_config.webui.port}',
'-e', f'ROOT_URL=http://{clp_config.webui.host}',
'-e', f'METEOR_SETTINGS={json.dumps(meteor_settings)}',
'-e', f'CLP_DB_HOST={clp_config.database.host}',
'-e', f'CLP_DB_PORT={clp_config.database.port}',
'-e', f'CLP_DB_NAME={clp_config.database.name}',
'-e', f'CLP_DB_USER={clp_config.database.username}',
'-e', f'CLP_DB_PASS={clp_config.database.password}',
'-e', f'WEBUI_LOGS_DIR={container_webui_logs_dir}',
'-e', f'WEBUI_LOGGING_LEVEL={clp_config.webui.logging_level}',
'-u', f'{os.getuid()}:{os.getgid()}',
]
necessary_mounts = [
mounts.clp_home,
DockerMount(DockerMountType.BIND, webui_logs_dir, container_webui_logs_dir),
]
for mount in necessary_mounts:
if mount:
container_cmd.append('--mount')
container_cmd.append(str(mount))
container_cmd.append(clp_config.execution_container)

node_cmd = [
str(CONTAINER_CLP_HOME / 'bin' / 'node'),
str(CONTAINER_CLP_HOME / 'var' / 'www' / 'launcher.js'),
str(CONTAINER_CLP_HOME / 'var' / 'www' / 'main.js')
]
cmd = container_cmd + node_cmd
subprocess.run(cmd, stdout=subprocess.DEVNULL, check=True)

logger.info(f"Started {WEBUI_COMPONENT_NAME}.")


def main(argv):
clp_home = get_clp_home()
default_config_file_path = clp_home / CLP_DEFAULT_CONFIG_FILE_RELATIVE_PATH
Expand All @@ -557,6 +624,7 @@ def main(argv):
component_args_parser.add_parser(QUEUE_COMPONENT_NAME)
component_args_parser.add_parser(RESULTS_CACHE_COMPONENT_NAME)
component_args_parser.add_parser(SCHEDULER_COMPONENT_NAME)
component_args_parser.add_parser(WEBUI_COMPONENT_NAME)
worker_args_parser = component_args_parser.add_parser(WORKER_COMPONENT_NAME)
worker_args_parser.add_argument('--num-cpus', type=int, default=0,
help="Number of logical CPU cores to use for compression")
Expand All @@ -581,7 +649,7 @@ def main(argv):

# Validate and load necessary credentials
if component_name in ['', DB_COMPONENT_NAME, SCHEDULER_COMPONENT_NAME,
SEARCH_SCHEDULER_COMPONENT_NAME]:
SEARCH_SCHEDULER_COMPONENT_NAME, WEBUI_COMPONENT_NAME]:
validate_and_load_db_credentials_file(clp_config, clp_home, True)
if component_name in ['', QUEUE_COMPONENT_NAME, SCHEDULER_COMPONENT_NAME,
WORKER_COMPONENT_NAME, SEARCH_SCHEDULER_COMPONENT_NAME,
Expand Down Expand Up @@ -635,6 +703,8 @@ def main(argv):
start_search_worker(instance_id, clp_config, container_clp_config, num_cpus, mounts)
if '' == component_name or WORKER_COMPONENT_NAME == component_name:
start_worker(instance_id, clp_config, container_clp_config, num_cpus, mounts)
if '' == component_name or WEBUI_COMPONENT_NAME == component_name:
start_webui(instance_id, clp_config, mounts)
except Exception as ex:
# Stop CLP
subprocess.run([str(clp_home / 'sbin' / 'stop-clp.sh')], check=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
SEARCH_SCHEDULER_COMPONENT_NAME,
SEARCH_WORKER_COMPONENT_NAME,
SCHEDULER_COMPONENT_NAME,
WORKER_COMPONENT_NAME
WORKER_COMPONENT_NAME,
WEBUI_COMPONENT_NAME,
)

# Setup logging
Expand Down Expand Up @@ -57,6 +58,7 @@ def main(argv):
component_args_parser.add_parser(RESULTS_CACHE_COMPONENT_NAME)
component_args_parser.add_parser(SCHEDULER_COMPONENT_NAME)
component_args_parser.add_parser(WORKER_COMPONENT_NAME)
component_args_parser.add_parser(WEBUI_COMPONENT_NAME)

parsed_args = args_parser.parse_args(argv[1:])

Expand Down Expand Up @@ -89,6 +91,8 @@ def main(argv):
with open(instance_id_file_path, 'r') as f:
instance_id = f.readline()

if '' == component_name or WEBUI_COMPONENT_NAME == component_name:
stop_container(f'clp-{WEBUI_COMPONENT_NAME}-{instance_id}')
if '' == component_name or WORKER_COMPONENT_NAME == component_name:
stop_container(f'clp-{WORKER_COMPONENT_NAME}-{instance_id}')
if '' == component_name or SEARCH_WORKER_COMPONENT_NAME == component_name:
Expand Down
35 changes: 35 additions & 0 deletions components/clp-py-utils/clp_py_utils/clp_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
SEARCH_SCHEDULER_COMPONENT_NAME = 'search_scheduler'
SEARCH_WORKER_COMPONENT_NAME = 'search_worker'
WORKER_COMPONENT_NAME = 'worker'
WEBUI_COMPONENT_NAME = 'webui'

CLP_DEFAULT_CREDENTIALS_FILE_PATH = pathlib.Path('etc') / 'credentials.yml'
CLP_METADATA_TABLE_PREFIX = 'clp_'
Expand Down Expand Up @@ -100,6 +101,7 @@ def get_clp_connection_params_and_type(self, disable_localhost_socket_connection
connection_params_and_type['ssl_cert'] = self.ssl_cert
return connection_params_and_type


def _validate_logging_level(cls, field):
if not is_valid_logging_level(field):
raise ValueError(
Expand Down Expand Up @@ -142,6 +144,12 @@ def validate_host(cls, field):
raise ValueError(f'{RESULTS_CACHE_COMPONENT_NAME}.host cannot be empty.')
return field

@validator('db_name')
def validate_db_name(cls, field):
if '' == field:
raise ValueError(f'{RESULTS_CACHE_COMPONENT_NAME}.db_name cannot be empty.')
return field

def get_uri(self):
return f"mongodb://{self.host}:{self.port}/{self.db_name}"

Expand Down Expand Up @@ -195,6 +203,32 @@ def dump_to_primitive_dict(self):
return d


class WebUi(BaseModel):
host: str = 'localhost'
port: int = 4000
logging_level: str = 'INFO'

@validator('host')
def validate_host(cls, field):
if '' == field:
raise ValueError(f'{WEBUI_COMPONENT_NAME}.host cannot be empty.')
return field

@validator('port')
def validate_port(cls, field):
min_valid_port = 0
max_valid_port = 2 ** 16 - 1
if min_valid_port > field or max_valid_port < field:
raise ValueError(f'{WEBUI_COMPONENT_NAME}.port is not within valid range '
f'{min_valid_port}-{max_valid_port}.')
return field

@validator('logging_level')
def validate_logging_level(cls, field):
_validate_logging_level(cls, field)
return field


class CLPConfig(BaseModel):
execution_container: str = 'ghcr.io/y-scope/clp/clp-execution-x86-ubuntu-focal:main'

Expand All @@ -206,6 +240,7 @@ class CLPConfig(BaseModel):
scheduler: Scheduler = Scheduler()
search_scheduler: SearchScheduler = SearchScheduler()
search_worker: SearchWorker = SearchWorker()
webui: WebUi = WebUi()
credentials_file_path: pathlib.Path = CLP_DEFAULT_CREDENTIALS_FILE_PATH

archive_output: ArchiveOutput = ArchiveOutput()
Expand Down
5 changes: 5 additions & 0 deletions components/package-template/src/etc/clp-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
# port: 27017
# db_name: "clp-search"
#
#webui:
# host: "localhost"
# port: 4000
# logging_level: "INFO"
#
#search_scheduler:
# jobs_poll_delay: 0.1 # seconds
# logging_level: "INFO"
Expand Down
1 change: 1 addition & 0 deletions components/webui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
Loading
Loading