Compare commits

...

94 Commits

Author SHA1 Message Date
64b4b201d5 Merge branch 'master' into embeddings 2023-08-19 15:48:15 -04:00
857f3315c2 Rework user scripts, update DB schema to support embeddings 2023-08-19 15:46:19 -04:00
5771693b1a Update README.md 2023-07-28 07:57:34 -04:00
27188b6fa0 wip 2023-07-24 19:36:20 -04:00
f56cfb0f2f Version bump, fix #392 2023-07-15 11:57:18 -04:00
70242846ae Rework sist2-admin UI 2023-07-14 21:19:59 -04:00
b833acf522 remove trailing slash in other endpoints 2023-07-14 20:13:32 -04:00
5fa2da5eef Update readme 2023-07-14 12:17:59 -04:00
ec518887ee version bump 2023-07-14 12:14:05 -04:00
0b0b7fe951 Fix stats page #387 2023-07-14 12:13:13 -04:00
ba863e4e6c Version bump 2023-07-14 11:35:11 -04:00
cbab4c2841 Remove leading slash in sist2-admin API requests #384 2023-07-14 11:20:05 -04:00
930361e78c Fix #388 2023-07-13 21:38:43 -04:00
92478ec47c Remove debug statement 2023-07-13 21:12:43 -04:00
0d81d7c43b Update openssl api #374 2023-07-13 20:49:45 -04:00
9f175cb0f0 Speedup CI build 2023-07-13 16:46:43 -04:00
6225cf81de Fix CI build scripts (pt. 2) 2023-07-13 16:29:30 -04:00
d7058ab645 Fix CI build scripts, update caniuse database 2023-07-13 16:23:04 -04:00
84958502b1 Fix websocket for #384 2023-07-12 19:39:52 -04:00
a0b6eed037 Fix arm64 dockerfile 2023-07-12 19:37:15 -04:00
06d6910151 Add test image 2023-07-12 19:36:17 -04:00
b99e4ddf13 npm audit fix 2023-07-12 19:36:17 -04:00
d14139ba44 Merge pull request #385 from simon987/dependabot/npm_and_yarn/sist2-admin/frontend/semver-5.7.2
Bump semver from 5.7.1 to 5.7.2 in /sist2-admin/frontend
2023-07-12 16:14:39 -04:00
dependabot[bot]
13960337aa Bump semver from 5.7.1 to 5.7.2 in /sist2-admin/frontend
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-12 15:28:54 +00:00
2596361af5 Use mupdf's OCR methods rather than raw tesseract, various fixes 2023-07-10 21:40:58 -04:00
5a1a04629f Fix #376 2023-07-01 09:21:02 -04:00
242dd67416 Fix #378 2023-07-01 09:06:03 -04:00
54d902146a Free tag json if parsing failed 2023-07-01 08:38:30 -04:00
3b0ab3679a Merge pull request #380 from jeaneric/Fix-tag
Fix tag
2023-07-01 08:37:21 -04:00
jeaneric
58ce0ef414 Fix tag
On my setup the cJSON_Delete corrupted the req object, releasing after fixed it.
2023-06-30 19:48:34 -04:00
f984baf7fd Fix #373 2023-06-10 10:59:25 -04:00
ce242d1053 Fix #372 2023-06-09 08:16:10 -04:00
71deab7fa2 Merge pull request #371 from dpieski/patch-3
Only remove files with job_name.
2023-06-08 18:02:20 -04:00
Andrew
b0462f9378 Only remove files with job_name. 2023-06-08 11:50:39 -05:00
ca845d80e8 Version bump 2023-06-07 20:40:45 -04:00
e2025df2c0 sist2-admin: don't set status to failed when using debug binary 2023-06-07 20:40:11 -04:00
7eb064162e close db connection before loop #346 2023-06-07 20:25:13 -04:00
7bc4b73e43 Use relative paths in sist2-admin #369 2023-06-07 19:59:50 -04:00
ca2e308d89 Bug fixes 2023-06-04 10:11:46 -04:00
c03c148273 SQLite backend support for sist2-admin #366 2023-06-03 18:33:44 -04:00
5522bcfa9b Add log cleanup features 2023-05-27 18:54:54 -04:00
f0fd708082 Fix timestamps in sist2-admin #359 2023-05-25 20:51:06 -04:00
6bf2b4c74d Merge remote-tracking branch 'origin/master' 2023-05-25 19:56:56 -04:00
d907576406 Update ES mapping for mtime #364 2023-05-25 19:56:46 -04:00
7659b481fa Merge pull request #365 from simon987/dependabot/npm_and_yarn/sist2-admin/frontend/socket.io-parser-4.2.3
Bump socket.io-parser from 4.2.2 to 4.2.3 in /sist2-admin/frontend
2023-05-24 08:52:16 -04:00
dependabot[bot]
e81e5ee457 Bump socket.io-parser from 4.2.2 to 4.2.3 in /sist2-admin/frontend
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.2 to 4.2.3.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.2...4.2.3)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-24 01:24:25 +00:00
d4820d2fad updates 2023-05-22 10:52:53 -04:00
b3b3005692 Update thumbnail-quality parameter in sist2-admin 2023-05-20 13:16:07 -04:00
610882112d Use WEBP to encode thumbnails 2023-05-20 13:12:12 -04:00
e2e0cf260f Skip encrypted files when no passphrase is supplied 2023-05-18 20:09:17 -04:00
3ffa30cc6f Only fetch ES aggregations on the first request (#357) 2023-05-18 19:56:40 -04:00
7920318406 Add polish localization (pt.2) 2023-05-18 19:52:15 -04:00
41ef940623 Add polish localization 2023-05-18 19:42:39 -04:00
cdec1cebc6 Add .devcontainer folder 2023-05-18 15:10:24 -04:00
0ce341d8e6 Fix print_errors in elastic.c 2023-05-18 15:10:00 -04:00
7d96d62983 Merge pull request #363 from simon987/sqlite-index
SQLite search backend
2023-05-18 14:29:34 -04:00
72293d26f2 code quality fixes 2023-05-18 14:26:49 -04:00
944c224904 SQLite search backend 2023-05-18 14:16:11 -04:00
63027dd5ca Merge pull request #354 from dpieski/patch-3
Use es-index WebOption when calling sist2.web
2023-04-27 14:46:59 -04:00
Andrew
ac942947e4 Use es-index WebOption when calling sist2.web 2023-04-27 12:57:52 -05:00
1cfceba518 wip 2023-04-25 08:49:50 -04:00
35cfd3b3b1 version bump 2023-04-23 18:26:55 -04:00
b286c652ad Fix tesseract language paths (pt 2) 2023-04-23 18:26:34 -04:00
2d8685f8f5 Fix tesseract language paths 2023-04-23 17:29:55 -04:00
c930ef7840 Update readme 2023-04-23 16:39:34 -04:00
d32bda0d68 Bug fixes 2023-04-23 14:15:31 -04:00
499ed0be79 Fix readme link 2023-04-23 12:54:33 -04:00
dc39c0ec4b Add NER support 2023-04-23 12:53:27 -04:00
b5cdd9a5df Work on README, optimize database storage 2023-04-22 16:02:19 -04:00
a8b6886f7b Fix stats page 2023-04-16 19:46:01 -04:00
a7e9b6af96 Flush documents in index 2023-04-15 13:49:18 -04:00
0710dc6d3d Update readme / format support 2023-04-15 13:39:25 -04:00
75b66b5982 Fix #351 2023-04-15 13:06:13 -04:00
9813646c11 Fix #343 2023-04-15 12:39:47 -04:00
ebc9468251 Fix some memory leaks 2023-04-15 11:54:56 -04:00
7baaca5078 add_work fix for problem in #349 pt 2 2023-04-15 09:48:50 -04:00
6c4bdc87cf add_work fix for problem in #349 2023-04-15 09:18:17 -04:00
1ea78887c3 Fix aarch64 build 2023-04-14 21:53:25 -04:00
886fa720ec Fix for ES 8.X #302 2023-04-14 21:48:29 -04:00
d43aac735f Add build flag to toggle debug info in web module 2023-04-14 21:07:48 -04:00
faf438a798 Add error message in home page on ES connection error #331 2023-04-14 20:51:35 -04:00
5b3b9911bd Bug fix for delete iterator 2023-04-13 18:35:36 -04:00
237d55ec9c Merge pull request #348 from simon987/dependabot/npm_and_yarn/sist2-vue/d3-color-and-d3-3.1.0
Bump d3-color and d3 in /sist2-vue
2023-04-10 20:40:35 -04:00
dependabot[bot]
ced4c7de88 Bump d3-color and d3 in /sist2-vue
Bumps [d3-color](https://github.com/d3/d3-color) to 3.1.0 and updates ancestor dependency [d3](https://github.com/d3/d3). These dependencies need to be updated together.


Updates `d3-color` from 1.4.1 to 3.1.0
- [Release notes](https://github.com/d3/d3-color/releases)
- [Commits](https://github.com/d3/d3-color/compare/v1.4.1...v3.1.0)

Updates `d3` from 5.16.0 to 7.8.4
- [Release notes](https://github.com/d3/d3/releases)
- [Changelog](https://github.com/d3/d3/blob/main/CHANGES.md)
- [Commits](https://github.com/d3/d3/compare/v5.16.0...v7.8.4)

---
updated-dependencies:
- dependency-name: d3-color
  dependency-type: indirect
- dependency-name: d3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-11 00:35:05 +00:00
90ee318981 Update CI build script 2023-04-10 20:07:52 -04:00
785121e46c Merge pull request #347 from simon987/dependabot/npm_and_yarn/sist2-admin/frontend/webpack-5.78.0
Bump webpack from 5.75.0 to 5.78.0 in /sist2-admin/frontend
2023-04-10 19:57:27 -04:00
585c57a2ad Fix antiword version 2023-04-10 19:57:02 -04:00
42abbbce95 Fix libmobi version 2023-04-10 19:54:05 -04:00
dependabot[bot]
e8607df26f Bump webpack from 5.75.0 to 5.78.0 in /sist2-admin/frontend
Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.78.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.78.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-10 23:51:06 +00:00
f1726ca0a9 Merge pull request #342 from simon987/dependabot/npm_and_yarn/sist2-vue/webpack-5.76.1
Bump webpack from 5.75.0 to 5.76.1 in /sist2-vue
2023-04-10 19:50:40 -04:00
3ef675abcf Merge pull request #345 from simon987/process-pool
Process pool
2023-04-10 19:50:23 -04:00
dependabot[bot]
81658efb19 Bump webpack from 5.75.0 to 5.76.1 in /sist2-vue
Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-15 10:25:29 +00:00
60c77678b4 Merge pull request #339 from einfachTobi/patch-1
Update messages.ts
2023-02-28 17:40:45 -05:00
einfachTobi
bf1d2f7d55 Update messages.ts 2023-02-28 11:24:02 +01:00
145 changed files with 15370 additions and 9413 deletions

9
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM simon987/sist2-build
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash
RUN apt update -y; apt install -y nodejs && rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=noninteractive
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8

View File

@@ -0,0 +1,16 @@
{
"name": "sist2-dev",
"dockerComposeFile": [
"docker-compose.yml"
],
"service": "sist2-dev",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.cpptools-extension-pack"
]
}
},
"remoteUser": "root",
"workspaceFolder": "/app/"
}

View File

@@ -0,0 +1,8 @@
version: "3"
services:
sist2-dev:
build: .
command: sleep infinity
volumes:
- ../:/app

View File

@@ -37,4 +37,5 @@ state.db
build/ build/
__pycache__/ __pycache__/
sist2-vue/dist sist2-vue/dist
sist2-admin/frontend/dist sist2-admin/frontend/dist
*.fts

3
.gitattributes vendored
View File

@@ -1,3 +0,0 @@
CMakeModules/* linguist-vendored
**/*_generated.c linguist-vendored
**/*_generated.h linguist-vendored

4
.gitignore vendored
View File

@@ -43,4 +43,6 @@ src/magic_generated.c
src/index/static_generated.c src/index/static_generated.c
*.sist2 *.sist2
*-shm *-shm
*-journal *-journal
.vscode
*.fts

View File

@@ -5,6 +5,7 @@ set(CMAKE_C_STANDARD 11)
option(SIST_DEBUG "Build a debug executable" on) option(SIST_DEBUG "Build a debug executable" on)
option(SIST_FAST "Enable more optimisation flags" off) option(SIST_FAST "Enable more optimisation flags" off)
option(SIST_DEBUG_INFO "Turn on debug information in web interface" on)
add_compile_definitions( add_compile_definitions(
"SIST_PLATFORM=${SIST_PLATFORM}" "SIST_PLATFORM=${SIST_PLATFORM}"
@@ -14,13 +15,24 @@ if (SIST_DEBUG)
add_compile_definitions( add_compile_definitions(
"SIST_DEBUG=${SIST_DEBUG}" "SIST_DEBUG=${SIST_DEBUG}"
) )
endif() set(VCPKG_BUILD_TYPE debug)
else ()
set(VCPKG_BUILD_TYPE release)
endif ()
if (SIST_DEBUG_INFO)
add_compile_definitions(
"SIST_DEBUG_INFO=${SIST_DEBUG_INFO}"
)
endif ()
add_subdirectory(third-party/libscan) add_subdirectory(third-party/libscan)
set(ARGPARSE_SHARED off) set(ARGPARSE_SHARED off)
add_subdirectory(third-party/argparse) add_subdirectory(third-party/argparse)
add_executable(sist2 add_executable(
sist2
# argparse # argparse
third-party/argparse/argparse.h third-party/argparse/argparse.c third-party/argparse/argparse.h third-party/argparse/argparse.c
@@ -47,7 +59,11 @@ add_executable(sist2
src/auth0/auth0_c_api.h src/auth0/auth0_c_api.cpp src/auth0/auth0_c_api.h src/auth0/auth0_c_api.cpp
src/database/database_stats.c src/database/database_stats.h src/database/database_schema.c) src/database/database_stats.c
src/database/database_schema.c
src/database/database_fts.c
src/web/web_fts.c
src/database/database_embeddings.c)
set_target_properties(sist2 PROPERTIES LINKER_LANGUAGE C) set_target_properties(sist2 PROPERTIES LINKER_LANGUAGE C)
target_link_directories(sist2 PRIVATE BEFORE ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/) target_link_directories(sist2 PRIVATE BEFORE ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/)
@@ -60,6 +76,7 @@ find_package(unofficial-mongoose CONFIG REQUIRED)
find_package(CURL CONFIG REQUIRED) find_package(CURL CONFIG REQUIRED)
find_library(MAGIC_LIB NAMES libmagic.a REQUIRED) find_library(MAGIC_LIB NAMES libmagic.a REQUIRED)
find_package(unofficial-sqlite3 CONFIG REQUIRED) find_package(unofficial-sqlite3 CONFIG REQUIRED)
find_package(OpenBLAS CONFIG REQUIRED)
target_include_directories( target_include_directories(
@@ -85,7 +102,7 @@ if (SIST_DEBUG)
-fno-omit-frame-pointer -fno-omit-frame-pointer
-fsanitize=address -fsanitize=address
-fno-inline -fno-inline
# -O2 # -O2
) )
target_link_options( target_link_options(
sist2 sist2
@@ -115,6 +132,7 @@ else ()
PRIVATE PRIVATE
-Ofast -Ofast
# -g
-fno-stack-protector -fno-stack-protector
-fomit-frame-pointer -fomit-frame-pointer
-w -w
@@ -141,6 +159,7 @@ target_link_libraries(
${MAGIC_LIB} ${MAGIC_LIB}
unofficial::sqlite3::sqlite3 unofficial::sqlite3::sqlite3
OpenBLAS::OpenBLAS
) )
add_custom_target( add_custom_target(

View File

@@ -1,11 +1,6 @@
FROM simon987/sist2-build as build FROM simon987/sist2-build as build
MAINTAINER simon987 <me@simon987.net> MAINTAINER simon987 <me@simon987.net>
ENV DEBIAN_FRONTEND=noninteractive
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash
RUN apt update -y; apt install -y nodejs && rm -rf /var/lib/apt/lists/*
WORKDIR /build/ WORKDIR /build/
COPY scripts scripts COPY scripts scripts
@@ -19,14 +14,12 @@ COPY sist2-admin sist2-admin
RUN cd sist2-vue/ && npm install && npm run build RUN cd sist2-vue/ && npm install && npm run build
RUN cd sist2-admin/frontend/ && npm install && npm run build RUN cd sist2-admin/frontend/ && npm install && npm run build
RUN mkdir build && cd build && cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .. RUN mkdir build && cd build && cmake -DSIST_PLATFORM=x64_linux_docker -DSIST_DEBUG_INFO=on -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake ..
RUN cd build && make -j$(nproc) RUN cd build && make -j$(nproc)
RUN strip build/sist2 || mv build/sist2_debug build/sist2 RUN strip build/sist2 || mv build/sist2_debug build/sist2
FROM --platform="linux/amd64" ubuntu@sha256:965fbcae990b0467ed5657caceaec165018ef44a4d2d46c7cdea80a9dff0d1ea FROM --platform="linux/amd64" ubuntu@sha256:965fbcae990b0467ed5657caceaec165018ef44a4d2d46c7cdea80a9dff0d1ea
WORKDIR /root
ENV LANG C.UTF-8 ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8 ENV LC_ALL C.UTF-8
@@ -37,21 +30,23 @@ RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y curl libasan5 li
RUN mkdir -p /usr/share/tessdata && \ RUN mkdir -p /usr/share/tessdata && \
cd /usr/share/tessdata/ && \ cd /usr/share/tessdata/ && \
curl -o /usr/share/tessdata/hin.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/hin.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/hin.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/hin.traineddata &&\
curl -o /usr/share/tessdata/jpn.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/jpn.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/jpn.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/jpn.traineddata &&\
curl -o /usr/share/tessdata/eng.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/eng.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/eng.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/eng.traineddata &&\
curl -o /usr/share/tessdata/fra.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/fra.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/fra.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/fra.traineddata &&\
curl -o /usr/share/tessdata/rus.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/rus.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/rus.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/rus.traineddata &&\
curl -o /usr/share/tessdata/osd.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/osd.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/osd.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/osd.traineddata &&\
curl -o /usr/share/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata &&\
curl -o /usr/share/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\
curl -o /usr/share/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\
curl -o /usr/share/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata curl -o /usr/share/tesseract-ocr/4.00/tessdata/pol.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/pol.traineddata &&\
curl -o /usr/share/tesseract-ocr/4.00/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata
# sist2 # sist2
COPY --from=build /build/build/sist2 /root/sist2 COPY --from=build /build/build/sist2 /root/sist2
# sist2-admin # sist2-admin
COPY sist2-admin/requirements.txt sist2-admin/ WORKDIR /root/sist2-admin
RUN python3 -m pip install --no-cache -r sist2-admin/requirements.txt COPY sist2-admin/requirements.txt /root/sist2-admin/
COPY --from=build /build/sist2-admin/ sist2-admin/ RUN python3 -m pip install --no-cache -r /root/sist2-admin/requirements.txt
COPY --from=build /build/sist2-admin/ /root/sist2-admin/

View File

@@ -1,9 +1,22 @@
FROM simon987/sist2-build-arm64 as build FROM simon987/sist2-build-arm64 as build
MAINTAINER simon987 <me@simon987.net> MAINTAINER simon987 <me@simon987.net>
WORKDIR /build/
COPY scripts scripts
COPY schema schema
COPY CMakeLists.txt .
COPY third-party third-party
COPY src src
COPY sist2-vue sist2-vue
COPY sist2-admin sist2-admin
RUN cd sist2-vue/ && npm install && npm run build
RUN cd sist2-admin/frontend/ && npm install && npm run build
WORKDIR /build/ WORKDIR /build/
ADD . /build/ ADD . /build/
RUN mkdir build && cd build && cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .. RUN mkdir build && cd build && cmake -DSIST_PLATFORM=arm64_linux_docker -DSIST_DEBUG_INFO=on -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake ..
RUN cd build && make -j$(nproc) RUN cd build && make -j$(nproc)
RUN strip build/sist2 || mv build/sist2_debug build/sist2 RUN strip build/sist2 || mv build/sist2_debug build/sist2
@@ -20,16 +33,17 @@ RUN apt update && apt install -y curl libasan5 libmagic1 tesseract-ocr python3-p
RUN mkdir -p /usr/share/tessdata && \ RUN mkdir -p /usr/share/tessdata && \
cd /usr/share/tessdata/ && \ cd /usr/share/tessdata/ && \
curl -o /usr/share/tessdata/hin.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/hin.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/hin.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/hin.traineddata &&\
curl -o /usr/share/tessdata/jpn.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/jpn.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/jpn.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/jpn.traineddata &&\
curl -o /usr/share/tessdata/eng.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/eng.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/eng.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/eng.traineddata &&\
curl -o /usr/share/tessdata/fra.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/fra.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/fra.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/fra.traineddata &&\
curl -o /usr/share/tessdata/rus.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/rus.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/rus.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/rus.traineddata &&\
curl -o /usr/share/tessdata/osd.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/osd.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/osd.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/osd.traineddata &&\
curl -o /usr/share/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata &&\
curl -o /usr/share/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\
curl -o /usr/share/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\
curl -o /usr/share/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata curl -o /usr/share/tesseract-ocr/4.00/tessdata/pol.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/pol.traineddata &&\
curl -o /usr/share/tesseract-ocr/4.00/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata
# sist2 # sist2
COPY --from=build /build/build/sist2 /root/sist2 COPY --from=build /build/build/sist2 /root/sist2

143
README.md
View File

@@ -10,13 +10,13 @@ sist2 (Simple incremental search tool)
*Warning: sist2 is in early development* *Warning: sist2 is in early development*
![search panel](docs/sist2.png) ![search panel](docs/sist2.gif)
## Features ## Features
* Fast, low memory usage, multi-threaded * Fast, low memory usage, multi-threaded
* Manage & schedule scan jobs with simple web interface (Docker only)
* Mobile-friendly Web interface * Mobile-friendly Web interface
* Portable (all its features are packaged in a single executable)
* Extracts text and metadata from common file types \* * Extracts text and metadata from common file types \*
* Generates thumbnails \* * Generates thumbnails \*
* Incremental scanning * Incremental scanning
@@ -24,47 +24,64 @@ sist2 (Simple incremental search tool)
* Recursive scan inside archive files \*\* * Recursive scan inside archive files \*\*
* OCR support with tesseract \*\*\* * OCR support with tesseract \*\*\*
* Stats page & disk utilisation visualization * Stats page & disk utilisation visualization
* Named-entity recognition (client-side) \*\*\*\*
\* See [format support](#format-support) \* See [format support](#format-support)
\*\* See [Archive files](#archive-files) \*\* See [Archive files](#archive-files)
\*\*\* See [OCR](#ocr) \*\*\* See [OCR](#ocr)
\*\*\*\* See [Named-Entity Recognition](#NER)
![stats](docs/stats.png)
## Getting Started ## Getting Started
1. Have an Elasticsearch (>= 6.8.X, ideally >=7.14.0) instance running ### Using Docker Compose *(Windows/Linux/Mac)*
1. Download [from official website](https://www.elastic.co/downloads/elasticsearch)
1. *(or)* Run using docker:
```bash
docker run -d -p 9200:9200 -e "discovery.type=single-node" elasticsearch:7.17.9
```
1. *(or)* Run using docker-compose:
```yaml
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms1G -Xmx2G"
```
1. Download sist2 executable
1. Download the [latest sist2 release](https://github.com/simon987/sist2/releases).
Select the file corresponding to your CPU architecture and mark the binary as executable with `chmod +x` *
2. *(or)* Download a [development snapshot](https://files.simon987.net/.gate/sist2/simon987_sist2/) *(Not
recommended!)*
3. *(or)* `docker pull simon987/sist2:2.12.1-x64-linux`
1. See [Usage guide](docs/USAGE.md) ```yaml
version: "3"
\* *Windows users*: **sist2** runs under [WSL](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux) services:
elasticsearch:
image: elasticsearch:7.17.9
restart: unless-stopped
environment:
- "discovery.type=single-node"
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
sist2-admin:
image: simon987/sist2:3.1.4-x64-linux
restart: unless-stopped
volumes:
- ./sist2-admin-data/:/sist2-admin/
- /:/host
ports:
- 4090:4090 # sist2
- 8080:8080 # sist2-admin
working_dir: /root/sist2-admin/
entrypoint: python3 /root/sist2-admin/sist2_admin/app.py
```
## Example usage Navigate to http://localhost:8080/ to configure sist2-admin.
See [Usage guide](docs/USAGE.md) for more details ### Using the executable file *(Linux/WSL only)*
1. Scan a directory: `sist2 scan ~/Documents -o ./docs_idx` 1. Choose search backend (See [comparison](#search-backends)):
1. Push index to Elasticsearch: `sist2 index ./docs_idx` * **Elasticsearch**: have an Elasticsearch (version >= 6.8.X, ideally >=7.14.0) instance running
1. Start web interface: `sist2 web ./docs_idx` 1. Download [from official website](https://www.elastic.co/downloads/elasticsearch)
2. *(or)* Run using docker:
```bash
docker run -d -p 9200:9200 -e "discovery.type=single-node" elasticsearch:7.17.9
```
* **SQLite**: No installation required
2. Download the [latest sist2 release](https://github.com/simon987/sist2/releases).
Select the file corresponding to your CPU architecture and mark the binary as executable with `chmod +x`.
3. See [usage guide](docs/USAGE.md) for command line usage.
Example usage:
1. Scan a directory: `sist2 scan ~/Documents --output ./documents.sist2`
2. Prepare search index:
* **Elasticsearch**: `sist2 index --es-url http://localhost:9200 ./documents.sist2`
* **SQLite**: `sist2 index --search-index ./search.sist2 ./documents.sist2`
3. Start web interface: `sist2 web ./documents.sist2`
## Format support ## Format support
@@ -82,7 +99,7 @@ See [Usage guide](docs/USAGE.md) for more details
| tar, zip, rar, 7z, ar ... | Libarchive | yes\* | - | no | | tar, zip, rar, 7z, ar ... | Libarchive | yes\* | - | no |
| docx, xlsx, pptx | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | if embedded | creator, modified_by, title | | docx, xlsx, pptx | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | if embedded | creator, modified_by, title |
| doc (MS Word 97-2003) | antiword | yes | no | author, title | | doc (MS Word 97-2003) | antiword | yes | no | author, title |
| mobi, azw, azw3 | libmobi | yes | no | author, title | | mobi, azw, azw3 | libmobi | yes | yes | author, title |
| wpd (WordPerfect) | libwpd | yes | no | *planned* | | wpd (WordPerfect) | libwpd | yes | no | *planned* |
| json, jsonl, ndjson | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | - | - | | json, jsonl, ndjson | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | - | - |
@@ -109,10 +126,10 @@ Download the language data files with your package manager (`apt install tessera
directly [from Github](https://github.com/tesseract-ocr/tesseract/wiki/Data-Files). directly [from Github](https://github.com/tesseract-ocr/tesseract/wiki/Data-Files).
The `simon987/sist2` image comes with common languages The `simon987/sist2` image comes with common languages
(hin, jpn, eng, fra, rus, spa, chi_sim, deu) pre-installed. (hin, jpn, eng, fra, rus, spa, chi_sim, deu, pol) pre-installed.
You can use the `+` separator to specify multiple languages. The language You can use the `+` separator to specify multiple languages. The language
name must be identical to the `*.traineddata` file installed on your system name must be identical to the `*.traineddata` file installed on your system
(use `chi_sim` rather than `chi-sim`). (use `chi_sim` rather than `chi-sim`).
Examples: Examples:
@@ -123,20 +140,61 @@ sist2 scan --ocr-images --ocr-lang eng ~/Images/Screenshots/
sist2 scan --ocr-ebooks --ocr-images --ocr-lang eng+chi_sim ~/Chinese-Bilingual/ sist2 scan --ocr-ebooks --ocr-images --ocr-lang eng+chi_sim ~/Chinese-Bilingual/
``` ```
### Search backends
sist2 v3.0.7+ supports SQLite search backend. The SQLite search backend has
fewer features and generally comparable query performance for medium-size
indices, but it uses much less memory and is easier to set up.
| | SQLite | Elasticsearch |
|----------------------------------------------|:---------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------:|
| Requires separate search engine installation | | ✓ |
| Memory footprint | ~20MB | >500MB |
| Query syntax | [fts5](https://www.sqlite.org/fts5.html) | [query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax) |
| Fuzzy search | | ✓ |
| Media Types tree real-time updating | | ✓ |
| Search in file `path` | [WIP](https://github.com/simon987/sist2/issues/402) | ✓ |
| Manual tagging | ✓ | ✓ |
| User scripts | ✓ | ✓ |
| Media Type breakdown for search results | | ✓ |
### NER
sist2 v3.0.4+ supports named-entity recognition (NER). Simply add a supported repository URL to
**Configuration** > **Machine learning options** > **Model repositories**
to enable it.
The text processing is done in your browser, no data is sent to any third-party services.
See [simon987/sist2-ner-models](https://github.com/simon987/sist2-ner-models) for more details.
#### List of available repositories:
| URL | Maintainer | Purpose |
|---------------------------------------------------------------------------------------------------------|-----------------------------------------|---------|
| [simon987/sist2-ner-models](https://raw.githubusercontent.com/simon987/sist2-ner-models/main/repo.json) | [simon987](https://github.com/simon987) | General |
<details>
<summary>Screenshot</summary>
![ner](docs/ner.png)
</details>
## Build from source ## Build from source
You can compile **sist2** by yourself if you don't want to use the pre-compiled binaries You can compile **sist2** by yourself if you don't want to use the pre-compiled binaries
### With docker (recommended) ### Using docker
```bash ```bash
git clone --recursive https://github.com/simon987/sist2/ git clone --recursive https://github.com/simon987/sist2/
cd sist2 cd sist2
docker build . -f ./Dockerfile -t my-sist2-image docker build . -t my-sist2-image
# Copy sist2 executable from docker image
docker run --rm --entrypoint cat my-sist2-image /root/sist2 > sist2-x64-linux docker run --rm --entrypoint cat my-sist2-image /root/sist2 > sist2-x64-linux
``` ```
### On a linux computer ### Using a linux computer
1. Install compile-time dependencies 1. Install compile-time dependencies
@@ -144,15 +202,14 @@ docker run --rm --entrypoint cat my-sist2-image /root/sist2 > sist2-x64-linux
apt install gcc g++ python3 yasm ragel automake autotools-dev wget libtool libssl-dev curl zip unzip tar xorg-dev libglu1-mesa-dev libxcursor-dev libxml2-dev libxinerama-dev gettext nasm git nodejs apt install gcc g++ python3 yasm ragel automake autotools-dev wget libtool libssl-dev curl zip unzip tar xorg-dev libglu1-mesa-dev libxcursor-dev libxml2-dev libxinerama-dev gettext nasm git nodejs
``` ```
1. Apply vcpkg patches, as per [sist2-build](https://github.com/simon987/sist2-build) Dockerfile 2. Install vcpkg using my fork: https://github.com/simon987/vcpkg
3. Install vcpkg dependencies
1. Install vcpkg dependencies
```bash ```bash
vcpkg install curl[core,openssl] sqlite3 cpp-jwt pcre cjson brotli libarchive[core,bzip2,libxml2,lz4,lzma,lzo] pthread tesseract libxml2 libmupdf gtest mongoose libmagic libraw gumbo ffmpeg[core,avcodec,avformat,swscale,swresample] vcpkg install openblas curl[core,openssl] sqlite3[core,fts5] cpp-jwt pcre cjson brotli libarchive[core,bzip2,libxml2,lz4,lzma,lzo] pthread tesseract libxml2 libmupdf[ocr] gtest mongoose libmagic libraw gumbo ffmpeg[core,avcodec,avformat,swscale,swresample,webp,opus,mp3lame,vpx,zlib]
``` ```
1. Build 4. Build
```bash ```bash
git clone --recursive https://github.com/simon987/sist2/ git clone --recursive https://github.com/simon987/sist2/
(cd sist2-vue; npm install; npm run build) (cd sist2-vue; npm install; npm run build)

View File

@@ -1,78 +1,68 @@
# Usage # Usage
*More examples (specifically with docker/compose) are in progress*
* [scan](#scan)
* [options](#scan-options)
* [examples](#scan-examples)
* [index format](#index-format)
* [index](#index)
* [options](#index-options)
* [examples](#index-examples)
* [web](#web)
* [options](#web-options)
* [examples](#web-examples)
* [rewrite_url](#rewrite_url)
* [elasticsearch](#elasticsearch)
* [exec-script](#exec-script)
* [tagging](#tagging)
* [sidecar files](#sidecar-files)
``` ```
Usage: sist2 scan [OPTION]... PATH Usage: sist2 scan [OPTION]... PATH
or: sist2 index [OPTION]... INDEX or: sist2 index [OPTION]... INDEX
or: sist2 sqlite-index [OPTION]... INDEX
or: sist2 web [OPTION]... INDEX... or: sist2 web [OPTION]... INDEX...
or: sist2 exec-script [OPTION]... INDEX
Lightning-fast file system indexer and search tool. Lightning-fast file system indexer and search tool.
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --version Show version and exit -v, --version Print version and exit.
--verbose Turn on logging --verbose Turn on logging.
--very-verbose Turn on debug messages --very-verbose Turn on debug messages.
--json-logs Output logs in JSON format.
Scan options Scan options
-t, --threads=<int> Number of threads. DEFAULT=1 -t, --threads=<int> Number of threads. DEFAULT: 1
--mem-throttle=<int> Total memory threshold in MiB for scan throttling. DEFAULT=0 -q, --thumbnail-quality=<int> Thumbnail quality, on a scale of 0 to 100, 100 being the best. DEFAULT: 50
-q, --thumbnail-quality=<int> Thumbnail quality, on a scale of 2 to 31, 2 being the best. DEFAULT=2 --thumbnail-size=<int> Thumbnail size, in pixels. DEFAULT: 552
--thumbnail-size=<int> Thumbnail size, in pixels. DEFAULT=500 --thumbnail-count=<int> Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails. DEFAULT: 1
--thumbnail-count=<int> Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails. DEFAULT=1 --content-size=<int> Number of bytes to be extracted from text documents. Set to 0 to disable. DEFAULT: 32768
--content-size=<int> Number of bytes to be extracted from text documents. Set to 0 to disable. DEFAULT=32768 -o, --output=<str> Output index file path. DEFAULT: index.sist2
--incremental=<str> Reuse an existing index and only scan modified files. --incremental If the output file path exists, only scan new or modified files.
-o, --output=<str> Output directory. DEFAULT=index.sist2/ --optimize-index Defragment index file after scan to reduce its file size.
--rewrite-url=<str> Serve files from this url instead of from disk. --rewrite-url=<str> Serve files from this url instead of from disk.
--name=<str> Index display name. DEFAULT: (name of the directory) --name=<str> Index display name. DEFAULT: index
--depth=<int> Scan up to DEPTH subdirectories deep. Use 0 to only scan files in PATH. DEFAULT: -1 --depth=<int> Scan up to DEPTH subdirectories deep. Use 0 to only scan files in PATH. DEFAULT: -1
--archive=<str> Archive file mode (skip|list|shallow|recurse). skip: Don't parse, list: only get file names as text, shallow: Don't parse archives inside archives. DEFAULT: recurse --archive=<str> Archive file mode (skip|list|shallow|recurse). skip: don't scan, list: only save file names as text, shallow: don't scan archives inside archives. DEFAULT: recurse
--archive-passphrase=<str> Passphrase for encrypted archive files --archive-passphrase=<str> Passphrase for encrypted archive files
--ocr-lang=<str> Tesseract language (use 'tesseract --list-langs' to see which are installed on your machine) --ocr-lang=<str> Tesseract language (use 'tesseract --list-langs' to see which are installed on your machine)
--ocr-images Enable OCR'ing of image files. --ocr-images Enable OCR'ing of image files.
--ocr-ebooks Enable OCR'ing of ebook files. --ocr-ebooks Enable OCR'ing of ebook files.
-e, --exclude=<str> Files that match this regex will not be scanned -e, --exclude=<str> Files that match this regex will not be scanned.
--fast Only index file names & mime type --fast Only index file names & mime type.
--treemap-threshold=<str> Relative size threshold for treemap (see USAGE.md). DEFAULT: 0.0005 --treemap-threshold=<str> Relative size threshold for treemap (see USAGE.md). DEFAULT: 0.0005
--mem-buffer=<int> Maximum memory buffer size per thread in MiB for files inside archives (see USAGE.md). DEFAULT: 2000 --mem-buffer=<int> Maximum memory buffer size per thread in MiB for files inside archives (see USAGE.md). DEFAULT: 2000
--read-subtitles Read subtitles from media files. --read-subtitles Read subtitles from media files.
--fast-epub Faster but less accurate EPUB parsing (no thumbnails, metadata) --fast-epub Faster but less accurate EPUB parsing (no thumbnails, metadata).
--checksums Calculate file checksums when scanning. --checksums Calculate file checksums when scanning.
--list-file=<str> Specify a list of newline-delimited paths to be scanned instead of normal directory traversal. Use '-' to read from stdin. --list-file=<str> Specify a list of newline-delimited paths to be scanned instead of normal directory traversal. Use '-' to read from stdin.
Index options Index options
-t, --threads=<int> Number of threads. DEFAULT=1 -t, --threads=<int> Number of threads. DEFAULT: 1
--es-url=<str> Elasticsearch url with port. DEFAULT=http://localhost:9200 --es-url=<str> Elasticsearch url with port. DEFAULT: http://localhost:9200
--es-index=<str> Elasticsearch index name. DEFAULT=sist2 --es-insecure-ssl Do not verify SSL connections to Elasticsearch.
-p, --print Just print JSON documents to stdout. --es-index=<str> Elasticsearch index name. DEFAULT: sist2
--incremental-index Conduct incremental indexing, assumes that the old index is already digested by Elasticsearch. -p, --print Print JSON documents to stdout instead of indexing to elasticsearch.
--incremental-index Conduct incremental indexing. Assumes that the old index is already ingested in Elasticsearch.
--script-file=<str> Path to user script. --script-file=<str> Path to user script.
--mappings-file=<str> Path to Elasticsearch mappings. --mappings-file=<str> Path to Elasticsearch mappings.
--settings-file=<str> Path to Elasticsearch settings. --settings-file=<str> Path to Elasticsearch settings.
--async-script Execute user script asynchronously. --async-script Execute user script asynchronously.
--batch-size=<int> Index batch size. DEFAULT: 100 --batch-size=<int> Index batch size. DEFAULT: 70
-f, --force-reset Reset Elasticsearch mappings and settings. (You must use this option the first time you use the index command) -f, --force-reset Reset Elasticsearch mappings and settings.
sqlite-index options
--search-index=<str> Path to search index. Will be created if it does not exist yet.
Web options Web options
--es-url=<str> Elasticsearch url. DEFAULT=http://localhost:9200 --es-url=<str> Elasticsearch url. DEFAULT: http://localhost:9200
--es-index=<str> Elasticsearch index name. DEFAULT=sist2 --es-insecure-ssl Do not verify SSL connections to Elasticsearch.
--bind=<str> Listen on this address. DEFAULT=localhost:4090 --search-index=<str> Path to SQLite search index.
--es-index=<str> Elasticsearch index name. DEFAULT: sist2
--bind=<str> Listen for connections on this address. DEFAULT: localhost:4090
--auth=<str> Basic auth in user:password format --auth=<str> Basic auth in user:password format
--auth0-audience=<str> API audience/identifier --auth0-audience=<str> API audience/identifier
--auth0-domain=<str> Application domain --auth0-domain=<str> Application domain
@@ -83,89 +73,18 @@ Web options
--dev Serve html & js files from disk (for development) --dev Serve html & js files from disk (for development)
--lang=<str> Default UI language. Can be changed by the user --lang=<str> Default UI language. Can be changed by the user
Exec-script options
--es-url=<str> Elasticsearch url. DEFAULT=http://localhost:9200
--es-index=<str> Elasticsearch index name. DEFAULT=sist2
--script-file=<str> Path to user script.
--async-script Execute user script asynchronously.
Made by simon987 <me@simon987.net>. Released under GPL-3.0 Made by simon987 <me@simon987.net>. Released under GPL-3.0
``` ```
## Scan
### Scan options
* `-t, --threads`
Number of threads for file parsing. **Do not set a number higher than `$(nproc)` or `$(Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors` in Windows!**
* `--mem-throttle`
Total memory threshold in MiB for scan throttling. Worker threads will not start a new parse job
until the total memory usage of sist2 is below this threshold. Set to 0 to disable. DEFAULT=0
* `-q, --thumbnail-quality`
Thumbnail quality, on a scale of 2 to 32, 2 being the best. See section below for a rough estimate of thumbnail database size
* `--thumbnail-size`
Thumbnail size in pixels.
* `--thumbnail-count`
Maximum number of thumbnails to generate. When set to a value >= 2, thumbnails for video previews
will be generated. The actual number of thumbnails generated depends on the length of the video (maximum 1 image
every ~7s). Set to 0 to completely disable thumbnails.
* `--content-size`
Number of bytes of text to be extracted from the content of files (plain text, PDFs etc.).
Repeated whitespace and special characters do not count toward this limit.
Set to 0 to completely disable content parsing.
* `--incremental`
Specify an existing index. Information about files in this index that were not modified (based on *mtime* attribute)
will be copied to the new index and will not be parsed again.
* `-o, --output` Output directory.
* `--rewrite-url` Set the `rewrite_url` option for the web module (See [rewrite_url](#rewrite_url))
* `--name` Set the `name` option for the web module
* `--depth` Maximum scan dept. Set to 0 only scan files directly in the root directory, set to -1 for infinite depth
* `--archive` Archive file mode.
* skip: Don't parse
* list: Only get file names as text
* shallow: Don't parse archives inside archives.
* recurse: Scan archives recursively (default)
* `--ocr-lang`, `--ocr-ebooks`, `--ocr-images` See [OCR](../README.md#OCR)
* `-e, --exclude` Regex pattern to exclude files. A file is excluded if the pattern matches any
part of the full absolute path.
Examples:
* `-e ".*\.ttf"`: Ignore ttf files
* `-e ".*\.(ttf|rar)"`: Ignore ttf and rar files
* `-e "^/mnt/backups/"`: Ignore all files in the `/mnt/backups/` directory
* `-e "^/mnt/Data[12]/"`: Ignore all files in the `/mnt/Data1/` and `/mnt/Data2/` directory
* `-e "(^/usr/)|(^/var/)|(^/media/DRIVE-A/tmp/)|(^/media/DRIVE-B/Trash/)"` Exclude the
`/usr`, `/var`, `/media/DRIVE-A/tmp`, `/media/DRIVE-B/Trash` directories
* `--fast` Only index file names and mime type
* `--treemap-threshold` Directories smaller than (`treemap-threshold` * `<total size of the index>`)
will not be considered for the disk utilisation visualization; their size will be added to
the parent directory. If the parent directory is still smaller than the threshold, it will also be "merged upwards"
and so on.
In effect, smaller `treemap-threshold` values will yield a more detailed
(but also a more cluttered and harder to read) visualization.
* `--mem-buffer` Maximum memory buffer size in MiB (per thread) for files inside archives. Media files
larger than this number will be read sequentially and no *seek* operations will be supported.
To check if a media file can be parsed without *seek*, execute `cat file.mp4 | ffprobe -`
* `--read-subtitles` When enabled, will attempt to read the subtitles stream from media files.
* `--fast-epub` Much faster but less accurate EPUB parsing. When enabled, sist2 will use a simple HTML parser to read epub files instead of the MuPDF library. No thumbnails are generated and author/title metadata are not parsed.
* `--checksums` Calculate file checksums (SHA1) when scanning files. This option does not cause any additional read
operations. Checksums are not calculated for all file types, unless the file is inside an archive. When enabled, duplicate
files are hidden in the web UI (this behaviour can be toggled in the Configuration page).
#### Thumbnail database size estimation #### Thumbnail database size estimation
See chart below for rough estimate of thumbnail size vs. thumbnail size & quality arguments: See chart below for rough estimate of thumbnail size vs. thumbnail size & quality arguments:
For example, `--thumbnail-size=500`, `--thumbnail-quality=2` for a directory with 8 million images will create a thumbnail database For example, `--thumbnail-size=500`, `--thumbnail-quality=50` for a directory with 8 million images will create a thumbnail database
that is about `8000000 * 36kB = 288GB`. that is about `8000000 * 11.8kB = 94.4GB`.
![thumbnail_size](thumbnail_size.png) ![thumbnail_size](thumbnail_size.png)
// TODO: add note about LMDB page size 4096
### Scan examples ### Scan examples
Simple scan Simple scan
@@ -175,130 +94,68 @@ sist2 scan ~/Documents
sist2 scan \ sist2 scan \
--threads 4 --content-size 16000000 --thumbnail-quality 2 --archive shallow \ --threads 4 --content-size 16000000 --thumbnail-quality 2 --archive shallow \
--name "My Documents" --rewrite-url "http://nas.domain.local/My Documents/" \ --name "My Documents" --rewrite-url "http://nas.domain.local/My Documents/" \
~/Documents -o ./documents.idx/ ~/Documents -o ./documents.sist2
``` ```
Incremental scan Incremental scan
```
sist2 scan --incremental ./orig_idx/ -o ./updated_idx/ ~/Documents
```
### Index format If the index file does not exist, `--incremental` has no effect.
A typical `ndjson` type index structure looks like this:
```
documents.idx/
├── descriptor.json
├── _index_main.ndjson.zst
├── treemap.csv
├── agg_mime.csv
├── agg_date.csv
├── add_size.csv
├── thumbs/
| ├── data.mdb
| └── lock.mdb
├── tags/
| ├── data.mdb
| └── lock.mdb
└── meta/
├── data.mdb
└── lock.mdb
```
The `_index_*.ndjson.zst` files contain the document data in JSON format, in a compressed newline-delemited file.
The `thumbs/` folder is a [LMDB](https://en.wikipedia.org/wiki/Lightning_Memory-Mapped_Database)
database containing the thumbnails.
The `descriptor.json` file contains general information about the index. The
following fields are safe to modify manually: `root`, `name`, [rewrite_url](#rewrite_url) and `timestamp`.
The `.csv` are pre-computed aggregations necessary for the stats page.
*thumbs/*:
LMDB key-value store. Keys are **binary** 16-byte md5 hash* (`_id` field)
and values are raw image bytes.
*\* Hash is calculated from the full path of the file, including the extension, relative to the index root*
## Index
### Index options
* `--es-url`
Elasticsearch url and port. If you are using docker, make sure that both containers are on the
same network.
* `--es-index`
Elasticsearch index name. DEFAULT=sist2
* `-p, --print`
Print index in JSON format to stdout.
* `--incremental-index`
Conduct incremental indexing. Assumes that the old index is already ingested in Elasticsearch.
Only the new changes since the last scan will be sent.
* `--script-file`
Path to user script. See [Scripting](scripting.md).
* `--mappings-file`
Path to custom Elasticsearch mappings. If none is specified, [the bundled mappings](https://github.com/simon987/sist2/tree/master/schema) will be used.
* `--settings-file`
Path to custom Elasticsearch settings. *(See above)*
* `--async-script`
Use `wait_for_completion=false` elasticsearch option while executing user script.
(See [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html))
* `--batch-size=<int>`
Index batch size. Indexing is generally faster with larger batches, but payloads that
are too large will fail and additional overhead for retrying with smaller sizes may slow
down the process.
* `-f, --force-reset`
Reset Elasticsearch mappings and settings.
* `-t, --threads` Number of threads to use. Ideally, choose a number equal to the number of logical cores of the machine hosting Elasticsearch.
### Index examples
**Push to elasticsearch**
```bash ```bash
sist2 index --force-reset --batch-size 1000 --es-url http://localhost:9200 ./my_index/ sist scan ~/Documents -o ./documents.sist2
sist2 index ./my_index/ sist scan ~/Documents -o ./documents.sist2 --incremental
# or
sist scan ~/Documents -o ./documents.sist2 --incremental
sist scan ~/Documents -o ./documents.sist2 --incremental
```
### Index documents to Elasticsearch search backend
```bash
sist2 index --force-reset --batch-size 1000 --es-url http://localhost:9200 ./my_index.sist2
sist2 index ./my_index.sist2
```
#### Index documents to SQLite search backend
```bash
# The search index will be created if it does not exist already
sist2 sqlite-index ./index1.sist2 --search-index search.sist2
sist2 sqlite-index ./index2.sist2 --search-index search.sist2
``` ```
**Save index in JSON format** **Save index in JSON format**
```bash ```bash
sist2 index --print ./my_index/ > my_index.ndjson sist2 index --print ./my_index.sist2 > my_index.ndjson
``` ```
**Inspect contents of an index** **Inspect contents of an index**
```bash ```bash
sist2 index --print ./my_index/ | jq | less sist2 index --print ./my_index.sist2 | jq | less
``` ```
## Web ## Web
### Web options
* `--es-url=<str>` Elasticsearch url.
* `--es-index`
Elasticsearch index name. DEFAULT=sist2
* `--bind=<str>` Listen on this address.
* `--auth=<str>` Basic auth in user:password format
* `--tag-auth=<str>` Basic auth in user:password format. Works the same way as the
`--auth` argument, but authentication is only applied the `/tag/` endpoint.
* `--tagline=<str>` When specified, will replace the default tagline in the navbar.
* `--dev` Serve html & js files from disk (for development, used to modify frontend files without having to recompile)
* `--lang=<str>` Set the default web UI language (See #180 for a list of supported languages, default
is `en`). The user can change the language in the configuration page
* `--auth0-audience`, `--auth0-domain`, `--auth0-client-id`, `--auth0-public-key-file` See [Authentication with Auth0](auth0.md)
### Web examples ### Web examples
**Single index** **Single index (Elasticsearch backend)**
```bash ```bash
sist2 web --auth admin:hunter2 --bind 0.0.0.0:8888 my_index sist2 web --auth admin:hunter2 --bind 0.0.0.0:8888 my_index.sist2
``` ```
**Multiple indices** **Multiple indices (Elasticsearch backend)**
```bash ```bash
# Indices will be displayed in this order in the web interface # Indices will be displayed in this order in the web interface
sist2 web index1 index2 index3 index4 sist2 web index1.sist2 index2.sist2 index3.sist2 index4.sist2
``` ```
**SQLite search backend**
```bash
sist2 web --search-index search.sist2 index1.sist2
```
#### Auth0 authentication
See [auth0.md](auth0.md)
### rewrite_url ### rewrite_url
When the `rewrite_url` field is not empty, the web module ignores the `root` When the `rewrite_url` field is not empty, the web module ignores the `root`
@@ -318,11 +175,6 @@ Using a version >=7.14.0 is recommended to enable the following features:
When using a legacy version of ES, a notice will be displayed next to the sist2 version in the web UI. When using a legacy version of ES, a notice will be displayed next to the sist2 version in the web UI.
If you don't care about the features above, you can ignore it or disable it in the configuration page. If you don't care about the features above, you can ignore it or disable it in the configuration page.
## exec-script
The `exec-script` command is used to execute a user script for an index that has already been imported to Elasticsearch with the `index` command. Note that the documents will not be reset to their default state before each execution as the `index` command does: if you make undesired changes to the documents by accident, you will need to run `index` again to revert to the original state.
# Tagging # Tagging
### Manual tagging ### Manual tagging
@@ -380,8 +232,8 @@ The sidecar file must have exactly the same file path and the `.s2meta` suffix.
``` ```
``` ```
sist2 scan ~/Documents -o ./docs.idx sist2 scan ~/Documents -o ./docs.sist2
sist2 index ./docs.idx sist2 index ./docs.sist2
``` ```
*NOTE*: It is technically possible to overwrite the `tag` value using sidecar files, however, *NOTE*: It is technically possible to overwrite the `tag` value using sidecar files, however,

BIN
docs/ner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

View File

@@ -1,18 +1,47 @@
## User scripts ## User scripts
*This document is under construction, more in-depth guide coming soon* User scripts are used to augment your sist2 index with additional metadata, neural network embeddings, tags etc.
Since version 3.2.0, user scripts are written in Python, and are ran against the sist2 index file. User scripts do not
need a connection to the search backend.
You can create a user script based on a template from the sist2-admin interface:
![sist2-admin-scripts](sist2-admin-scripts.png)
User scripts leverage the [sist2-python](https://github.com/simon987/sist2-python) library to interface with the
index file*. You can find sist2-python documentation and examples
here: [sist2-python.readthedocs.io](https://sist2-python.readthedocs.io/).
If you are not using the sist2-admin interface, you can run user scripts manually from the command line:
```
pip install git+https://github.com/simon987/sist2-python.git
python my_script.py /path/to/my_index.sist2
```
\* It is possible to manually update the index using raw SQL queries, but the database schema is not stable and
can change at any time; it is recommended to use the more stable sist2-python wrapper instead.
<hr>
<details>
<summary>Legacy user scripts (sist2 version < 3.2.0)</summary>
During the `index` step, you can use the `--script-file <script>` option to During the `index` step, you can use the `--script-file <script>` option to
modify documents or add user tags. This option is mainly used to modify documents or add user tags. This option is mainly used to
implement automatic tagging based on file attributes. implement automatic tagging based on file attributes.
The scripting language used The scripting language used
([Painless Scripting Language](https://www.elastic.co/guide/en/elasticsearch/painless/7.4/index.html)) ([Painless Scripting Language](https://www.elastic.co/guide/en/elasticsearch/painless/7.4/index.html))
is very similar to Java, but you should be able to create user scripts is very similar to Java, but you should be able to create user scripts
without programming experience at all if you're somewhat familiar with without programming experience at all if you're somewhat familiar with
regex. regex.
This is the base structure of the documents we're working with: This is the base structure of the documents we're working with:
```json ```json
{ {
"_id": "e171405c-fdb5-4feb-bb32-82637bc32084", "_id": "e171405c-fdb5-4feb-bb32-82637bc32084",
@@ -34,7 +63,8 @@ This is the base structure of the documents we're working with:
**Example script** **Example script**
This script checks if the `genre` attribute exists, if it does This script checks if the `genre` attribute exists, if it does
it adds the `genre.<genre>` tag. it adds the `genre.<genre>` tag.
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
@@ -47,21 +77,23 @@ You can use `.` to create a hierarchical tag tree:
![scripting/genre_example](genre_example.png) ![scripting/genre_example](genre_example.png)
To use regular expressions, you need to add this line in `/etc/elasticsearch/elasticsearch.yml` To use regular expressions, you need to add this line in `/etc/elasticsearch/elasticsearch.yml`
```yaml ```yaml
script.painless.regex.enabled: true script.painless.regex.enabled: true
``` ```
Or, if you're using docker add `-e "script.painless.regex.enabled=true"` Or, if you're using docker add `-e "script.painless.regex.enabled=true"`
**Tag color** **Tag color**
You can specify the color for an individual tag by appending an You can specify the color for an individual tag by appending an
hexadecimal color code (`#RRGGBBAA`) to the tag name. hexadecimal color code (`#RRGGBBAA`) to the tag name.
### Examples ### Examples
If `(20XX)` is in the file name, add the `year.<year>` tag: If `(20XX)` is in the file name, add the `year.<year>` tag:
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
@@ -72,6 +104,7 @@ if (m.find()) {
``` ```
Use default *Calibre* folder structure to infer author. Use default *Calibre* folder structure to infer author.
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
@@ -84,8 +117,9 @@ if (ctx._source.name.contains("-") && ctx._source.extension == "pdf") {
} }
``` ```
If the file matches a specific pattern `AAAA-000 fName1 lName1, <fName2 lName2>...`, add the `actress.<actress>` and If the file matches a specific pattern `AAAA-000 fName1 lName1, <fName2 lName2>...`, add the `actress.<actress>` and
`studio.<studio>` tag: `studio.<studio>` tag:
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
@@ -102,16 +136,18 @@ if (m.find()) {
``` ```
Set the name of the last folder (`/path/to/<studio>/file.mp4`) to `studio.<studio>` tag Set the name of the last folder (`/path/to/<studio>/file.mp4`) to `studio.<studio>` tag
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
if (ctx._source.path != "") { if (ctx._source.path != "") {
String[] names = ctx._source.path.splitOnToken('/'); String[] names = ctx._source.path.splitOnToken('/');
tags.add("studio." + names[names.length-1]); tags.add("studio." + names[names.length-1]);
} }
``` ```
Parse `EXIF:F Number` tag Parse `EXIF:F Number` tag
```Java ```Java
if (ctx._source?.exif_fnumber != null) { if (ctx._source?.exif_fnumber != null) {
String[] values = ctx._source.exif_fnumber.splitOnToken(' '); String[] values = ctx._source.exif_fnumber.splitOnToken(' ');
@@ -124,6 +160,7 @@ if (ctx._source?.exif_fnumber != null) {
``` ```
Display year and months from `EXIF:DateTime` tag Display year and months from `EXIF:DateTime` tag
```Java ```Java
if (ctx._source?.exif_datetime != null) { if (ctx._source?.exif_datetime != null) {
SimpleDateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); SimpleDateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
@@ -140,3 +177,6 @@ if (ctx._source?.exif_datetime != null) {
} }
``` ```
</details>

BIN
docs/sist2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 169 KiB

View File

@@ -68,7 +68,7 @@
}, },
"mtime": { "mtime": {
"type": "date", "type": "date",
"format": "epoch_millis" "format": "epoch_second"
}, },
"size": { "size": {
"type": "long" "type": "long"
@@ -202,6 +202,46 @@
}, },
"modified_by": { "modified_by": {
"type": "text" "type": "text"
},
"emb.384.*": {
"type": "dense_vector",
"dims": 384
},
"emb.idx_384.*": {
"type": "dense_vector",
"dims": 384,
"index": true,
"similarity": "cosine"
},
"emb.idx_512.clip": {
"type": "dense_vector",
"dims": 512,
"index": true,
"similarity": "cosine"
},
"emb.512.*": {
"type": "dense_vector",
"dims": 512
},
"emb.idx_768.*": {
"type": "dense_vector",
"dims": 768,
"index": true,
"similarity": "cosine"
},
"emb.768.*": {
"type": "dense_vector",
"dims": 768
},
"emb.idx_1024.*": {
"type": "dense_vector",
"dims": 1024,
"index": true,
"similarity": "cosine"
},
"emb.1024.*": {
"type": "dense_vector",
"dims": 1024
} }
} }
} }

View File

@@ -4,14 +4,34 @@ VCPKG_ROOT="/vcpkg"
git submodule update --init --recursive git submodule update --init --recursive
rm -rf CMakeFiles CMakeCache.txt (
cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . cd sist2-vue/
make -j $(nproc) npm install
strip sist2 npm run build
./sist2 -v > VERSION ) &
mv sist2 sist2-x64-linux
rm -rf CMakeFiles CMakeCache.txt (
cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . cd sist2-admin/frontend/
make -j $(nproc) npm install
mv sist2_debug sist2-x64-linux-debug npm run build
) &
wait
mkdir build
(
cd build
cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG_INFO=on -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" ..
make -j $(nproc)
strip sist2
./sist2 -v > VERSION
)
mv build/sist2 sist2-x64-linux
(
cd build
rm -rf CMakeFiles CMakeCache.txt
cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG_INFO=on -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" ..
make -j $(nproc)
)
mv build/sist2_debug sist2-x64-linux-debug

View File

@@ -4,14 +4,33 @@ VCPKG_ROOT="/vcpkg"
git submodule update --init --recursive git submodule update --init --recursive
rm -rf CMakeFiles CMakeCache.txt (
cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . cd sist2-vue/
make -j $(nproc) npm install
strip sist2 npm run build
mv sist2 sist2-arm64-linux ) &
(
cd sist2-admin/frontend/
npm install
npm run build
) &
wait
mkdir build
(
cd build
cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG_INFO=on -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" ..
make -j $(nproc)
strip sist2
)
mv build/sist2 sist2-arm64-linux
rm -rf CMakeFiles CMakeCache.txt rm -rf CMakeFiles CMakeCache.txt
cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . (
make -j $(nproc) cd build
strip sist2 cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG_INFO=on -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" ..
mv sist2_debug sist2-arm64-linux-debug make -j $(nproc)
)
mv build/sist2_debug sist2-arm64-linux-debug

View File

@@ -0,0 +1,131 @@
import sqlite3
import orjson as json
import os
import string
from hashlib import md5
import random
from tqdm import tqdm
schema = """
CREATE TABLE thumbnail (
id TEXT NOT NULL CHECK (
length(id) = 32
),
num INTEGER NOT NULL,
data BLOB NOT NULL,
PRIMARY KEY(id, num)
) WITHOUT ROWID;
CREATE TABLE version (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE TABLE document (
id TEXT PRIMARY KEY NOT NULL CHECK (
length(id) = 32
),
marked INTEGER NOT NULL DEFAULT (1),
version INTEGER NOT NULL REFERENCES version(id),
mtime INTEGER NOT NULL,
size INTEGER NOT NULL,
json_data TEXT NOT NULL CHECK (
json_valid(json_data)
)
);
CREATE TABLE delete_list (
id TEXT PRIMARY KEY CHECK (
length(id) = 32
)
) WITHOUT ROWID;
CREATE TABLE tag (
id TEXT NOT NULL,
tag TEXT NOT NULL,
PRIMARY KEY (id, tag)
);
CREATE TABLE document_sidecar (
id TEXT PRIMARY KEY NOT NULL, json_data TEXT NOT NULL
) WITHOUT ROWID;
CREATE TABLE descriptor (
id TEXT NOT NULL, version_major INTEGER NOT NULL,
version_minor INTEGER NOT NULL, version_patch INTEGER NOT NULL,
root TEXT NOT NULL, name TEXT NOT NULL,
rewrite_url TEXT, timestamp INTEGER NOT NULL
);
CREATE TABLE stats_treemap (
path TEXT NOT NULL, size INTEGER NOT NULL
);
CREATE TABLE stats_size_agg (
bucket INTEGER NOT NULL, count INTEGER NOT NULL
);
CREATE TABLE stats_date_agg (
bucket INTEGER NOT NULL, count INTEGER NOT NULL
);
CREATE TABLE stats_mime_agg (
mime TEXT NOT NULL, size INTEGER NOT NULL,
count INTEGER NOT NULL
);
CREATE TABLE embedding (
id TEXT REFERENCES document(id),
model_id INTEGER NOT NULL references model(id),
start INTEGER NOT NULL,
end INTEGER,
embedding BLOB NOT NULL,
PRIMARY KEY (id, model_id, start)
);
CREATE TABLE model (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE CHECK (
length(name) < 16
),
url TEXT,
path TEXT NOT NULL UNIQUE,
size INTEGER NOT NULL,
type TEXT NOT NULL CHECK (
type IN ('flat', 'nested')
)
);
"""
content = "".join(random.choices(string.ascii_letters, k=500))
def gen_document():
return [
md5(random.randbytes(8)).hexdigest(),
json.dumps({
"content": content,
"mime": "image/jpeg",
"extension": "jpeg",
"name": "test",
"path": "",
})
]
if __name__ == "__main__":
DB_NAME = "big_index.sist2"
SIZE = 30_000_000
os.remove(DB_NAME)
db = sqlite3.connect(DB_NAME)
db.executescript(schema)
db.executescript("""
PRAGMA journal_mode = OFF;
PRAGMA synchronous = 0;
""")
for _ in tqdm(range(SIZE), total=SIZE):
db.execute(
"INSERT INTO document (id, version, mtime, size, json_data) VALUES (?, 1, 1000000, 10000, ?)",
gen_document()
)
# 1. Enable rowid from document
# 2. CREATE TABLE marked (
# id INTEGER PRIMARY KEY,
# marked int
# );
# 3. Set FK for document_sidecar, embedding, tag, thumbnail
# 4. Toggle FK if debug
db.commit()

View File

@@ -0,0 +1,84 @@
#include <sqlite3ext.h>
#include <string.h>
#include <stdlib.h>
SQLITE_EXTENSION_INIT1
static int sep_rfind(const char *str) {
for (int i = (int) strlen(str); i >= 0; i--) {
if (str[i] == '/') {
return i;
}
}
return -1;
}
void path_parent_func(sqlite3_context *ctx, int argc, sqlite3_value **argv) {
if (argc != 1 || sqlite3_value_type(argv[0]) != SQLITE_TEXT) {
sqlite3_result_error(ctx, "Invalid parameters", -1);
}
const char *value = (const char *) sqlite3_value_text(argv[0]);
int stop = sep_rfind(value);
if (stop == -1) {
sqlite3_result_null(ctx);
return;
}
char parent[4096 * 3];
strncpy(parent, value, stop);
sqlite3_result_text(ctx, parent, stop, SQLITE_TRANSIENT);
}
void random_func(sqlite3_context *ctx, int argc, sqlite3_value **argv) {
if (argc != 1 || sqlite3_value_type(argv[0]) != SQLITE_INTEGER) {
sqlite3_result_error(ctx, "Invalid parameters", -1);
}
char state_buf[32] = {0,};
struct random_data buf;
int result;
long seed = sqlite3_value_int64(argv[0]);
initstate_r((int) seed, state_buf, sizeof(state_buf), &buf);
random_r(&buf, &result);
sqlite3_result_int(ctx, result);
}
int sqlite3_extension_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi
) {
SQLITE_EXTENSION_INIT2(pApi);
sqlite3_create_function(
db,
"path_parent",
1,
SQLITE_UTF8,
NULL,
path_parent_func,
NULL,
NULL
);
sqlite3_create_function(
db,
"random_seeded",
1,
SQLITE_UTF8,
NULL,
random_func,
NULL,
NULL
);
return SQLITE_OK;
}

View File

@@ -0,0 +1 @@
gcc -I/mnt/work/vcpkg/installed/x64-linux/include -g -fPIC -shared sqlite_extension.c -o sist2funcs.so

View File

@@ -1,3 +1,3 @@
docker run --rm -it --name "sist2-dev-es"\ docker run --rm -it --name "sist2-dev-es3"\
-p 9200:9200 -e "discovery.type=single-node" \ -p 9200:9200 -e "discovery.type=single-node" \
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:7.17.9 -e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:7.17.9

View File

@@ -1,3 +1,3 @@
docker run --rm -it --name "sist2-dev-es"\ docker run --rm -it --name "sist2-dev-es3"\
-p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" \ -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" \
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:8.1.2 -e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:8.7.0

View File

@@ -0,0 +1,7 @@
docker build . -t tmp
docker run --rm -it\
-v $(pwd):/host \
tmp \
scan --ocr-lang eng --ocr-ebooks -t6 --incremental --very-verbose \
-o /host/docker.sist2 /host/third-party/libscan/libscan-test-files/test_files/

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,15 @@
<template> <template>
<div id="app"> <div id="app">
<NavBar></NavBar> <NavBar></NavBar>
<b-container class="pt-4"> <b-container class="pt-4">
<b-alert show dismissible variant="info"> <b-alert show dismissible variant="info">
This is a beta version of sist2-admin. Please submit bug reports, usability issues and feature requests This is a beta version of sist2-admin. Please submit bug reports, usability issues and feature requests
to the <a href="https://github.com/simon987/sist2/issues/new/choose" target="_blank">issue tracker on Github</a>. Thank you! to the <a href="https://github.com/simon987/sist2/issues/new/choose" target="_blank">issue tracker on
</b-alert> Github</a>. Thank you!
<router-view/> </b-alert>
</b-container> <router-view v-if="$store.state.sist2AdminInfo"/>
</div> </b-container>
</div>
</template> </template>
<script> <script>
@@ -16,83 +17,89 @@ import NavBar from "@/components/NavBar";
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
export default { export default {
components: {NavBar}, components: {NavBar},
data() { data() {
return { return {
socket: null socket: null
}
},
mounted() {
Sist2AdminApi.getSist2AdminInfo()
.then(resp => this.$store.commit("setSist2AdminInfo", resp.data));
this.$store.dispatch("loadBrowserSettings");
this.connectNotifications();
// this.socket.onclose = this.connectNotifications;
},
methods: {
connectNotifications() {
this.socket = new WebSocket(`ws://${window.location.host}/notifications`);
this.socket.onopen = () => {
this.socket.send("Hello from client");
}
this.socket.onmessage = e => {
const notification = JSON.parse(e.data);
if (notification.message) {
notification.messageString = this.$t(notification.message).toString();
} }
},
mounted() {
Sist2AdminApi.getSist2AdminInfo()
.then(resp => this.$store.commit("setSist2AdminInfo", resp.data));
this.$store.dispatch("loadBrowserSettings");
this.connectNotifications();
// this.socket.onclose = this.connectNotifications;
},
methods: {
connectNotifications() {
if (window.location.protocol === "https:") {
this.socket = new WebSocket(`wss://${window.location.host}/notifications`);
} else {
this.socket = new WebSocket(`ws://${window.location.host}/notifications`);
}
this.socket.onopen = () => {
this.socket.send("Hello from client");
}
this.$store.dispatch("notify", notification) this.socket.onmessage = e => {
} const notification = JSON.parse(e.data);
if (notification.message) {
notification.messageString = this.$t(notification.message).toString();
}
this.$store.dispatch("notify", notification)
}
}
} }
}
} }
</script> </script>
<style> <style>
html, body { html, body {
height: 100%; height: 100%;
} }
#app { #app {
/*font-family: Avenir, Helvetica, Arial, sans-serif;*/ /*font-family: Avenir, Helvetica, Arial, sans-serif;*/
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
/*text-align: center;*/ /*text-align: center;*/
color: #2c3e50; color: #2c3e50;
padding-bottom: 1em; padding-bottom: 1em;
min-height: 100%; min-height: 100%;
} }
.info-icon { .info-icon {
width: 1rem; width: 1rem;
margin-right: 0.2rem; min-width: 1rem;
cursor: pointer; margin-right: 0.2rem;
line-height: 1rem; cursor: pointer;
height: 1rem; line-height: 1rem;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==); height: 1rem;
filter: brightness(45%); min-height: 1rem;
display: block; background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==);
filter: brightness(45%);
display: block;
} }
.tabs { .tabs {
margin-top: 10px; margin-top: 10px;
} }
.modal-title { .modal-title {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
@media screen and (min-width: 1500px) { @media screen and (min-width: 1500px) {
.container { .container {
max-width: 1440px; max-width: 1440px;
} }
} }
label { label {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0; margin-bottom: 0;
} }
</style> </style>

View File

@@ -7,15 +7,15 @@ class Sist2AdminApi {
} }
getJobs() { getJobs() {
return axios.get(`${this.baseUrl}/api/job/`); return axios.get(`${this.baseUrl}/api/job`);
} }
getFrontends() { getFrontends() {
return axios.get(`${this.baseUrl}/api/frontend/`); return axios.get(`${this.baseUrl}/api/frontend`);
} }
getTasks() { getTasks() {
return axios.get(`${this.baseUrl}/api/task/`); return axios.get(`${this.baseUrl}/api/task`);
} }
killTask(taskId) { killTask(taskId) {
@@ -33,9 +33,26 @@ class Sist2AdminApi {
return axios.get(`${this.baseUrl}/api/job/${name}`); return axios.get(`${this.baseUrl}/api/job/${name}`);
} }
/** getSearchBackend(name) {
* @param {string} name return axios.get(`${this.baseUrl}/api/search_backend/${name}`);
*/ }
updateSearchBackend(name, data) {
return axios.put(`${this.baseUrl}/api/search_backend/${name}`, data);
}
getSearchBackends() {
return axios.get(`${this.baseUrl}/api/search_backend`);
}
deleteBackend(name) {
return axios.delete(`${this.baseUrl}/api/search_backend/${name}`)
}
createBackend(name) {
return axios.post(`${this.baseUrl}/api/search_backend/${name}`);
}
getFrontend(name) { getFrontend(name) {
return axios.get(`${this.baseUrl}/api/frontend/${name}`); return axios.get(`${this.baseUrl}/api/frontend/${name}`);
} }
@@ -110,7 +127,49 @@ class Sist2AdminApi {
} }
getSist2AdminInfo() { getSist2AdminInfo() {
return axios.get(`${this.baseUrl}/api/`); return axios.get(`${this.baseUrl}/api`);
}
getLogsToDelete(jobName, n) {
return axios.get(`${this.baseUrl}/api/job/${jobName}/logs_to_delete`, {
params: {n: n}
});
}
deleteTaskLogs(taskId) {
return axios.post(`${this.baseUrl}/api/task/${taskId}/delete_logs`);
}
getUserScripts() {
return axios.get(`${this.baseUrl}/api/user_script`);
}
getUserScript(name) {
return axios.get(`${this.baseUrl}/api/user_script/${name}`);
}
createUserScript(name, template) {
return axios.post(`${this.baseUrl}/api/user_script/${name}`, null, {
params: {
template: template
}
});
}
updateUserScript(name, data) {
return axios.put(`${this.baseUrl}/api/user_script/${name}`, data);
}
deleteUserScript(name) {
return axios.delete(`${this.baseUrl}/api/user_script/${name}`);
}
testUserScript(name, job) {
return axios.get(`${this.baseUrl}/api/user_script/${name}/run`, {
params: {
job: job
}
});
} }
} }

View File

@@ -1,64 +0,0 @@
<template>
<div>
<label>{{ $t("indexOptions.threads") }}</label>
<b-form-input v-model="options.threads" type="number" min="1" @change="update()"></b-form-input>
<label>{{ $t("webOptions.esUrl") }}</label>
<b-alert :variant="esTestOk ? 'success' : 'danger'" :show="showEsTestAlert" class="mt-1">
{{ esTestMessage }}
</b-alert>
<b-input-group>
<b-form-input v-model="options.es_url" @change="update()"></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="testEs()">{{ $t("test") }}</b-button>
</b-input-group-append>
</b-input-group>
<label>{{ $t("indexOptions.esIndex") }}</label>
<b-form-input v-model="options.es_index" @change="update()"></b-form-input>
<br>
<b-form-checkbox v-model="options.es_insecure_ssl" :disabled="!options.es_url.startsWith('https')" @change="update()">
{{ $t("webOptions.esInsecure") }}
</b-form-checkbox>
<label>{{ $t("indexOptions.batchSize") }}</label>
<b-form-input v-model="options.batch_size" type="number" min="1" @change="update()"></b-form-input>
<label>{{ $t("indexOptions.script") }}</label>
<b-form-textarea v-model="options.script" rows="6" @change="update()"></b-form-textarea>
</div>
</template>
<script>
import sist2AdminApi from "@/Sist2AdminApi";
export default {
name: "IndexOptions",
props: ["options"],
data() {
return {
showEsTestAlert: false,
esTestOk: false,
esTestMessage: "",
}
},
methods: {
update() {
this.$emit("change", this.options);
},
testEs() {
sist2AdminApi.pingEs(this.options.es_url, this.options.es_insecure_ssl).then((resp) => {
this.showEsTestAlert = true;
this.esTestOk = resp.data.ok;
this.esTestMessage = resp.data.message;
});
}
},
}
</script>
<style scoped>
</style>

View File

@@ -1,42 +1,54 @@
<template> <template>
<div> <div>
<h5>{{ $t("selectJobs") }}</h5> <h5>{{ $t("selectJobs") }}</h5>
<b-progress v-if="loading" striped animated value="100"></b-progress> <b-progress v-if="loading" striped animated value="100"></b-progress>
<b-form-group v-else> <b-form-group v-else>
<b-form-checkbox-group <b-form-checkbox-group
v-if="jobs.length > 0" v-if="jobs.length > 0"
:checked="frontend.jobs" :checked="frontend.jobs"
@input="frontend.jobs = $event; $emit('input')" @input="frontend.jobs = $event; $emit('input')"
> >
<div v-for="job in jobs" :key="job.name"> <div v-for="job in jobs" :key="job.name">
<b-form-checkbox :disabled="job.status !== 'indexed'" :value="job.name">[{{ job.name }}]</b-form-checkbox> <b-form-checkbox :disabled="job.status !== 'indexed'"
<br/> :value="job.name">
</div> <template #default><span
</b-form-checkbox-group> :title="job.status !== 'indexed' ? $t('jobOptions.notIndexed') : ''"
<div v-else> >[{{ job.name }}]</span></template>
<span class="text-muted">{{ $t('jobOptions.noJobAvailable') }}</span> </b-form-checkbox>
&nbsp;<router-link to="/">{{$t("create")}}</router-link> <br/>
</div> </div>
</b-form-group> </b-form-checkbox-group>
</div> <div v-else>
<span class="text-muted">{{ $t('jobOptions.noJobAvailable') }}</span>
<router-link to="/">{{ $t("create") }}</router-link>
</div>
</b-form-group>
</div>
</template> </template>
<script> <script>
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
export default { export default {
name: "JobCheckboxGroup", name: "JobCheckboxGroup",
props: ["frontend"], props: ["frontend"],
mounted() { mounted() {
Sist2AdminApi.getJobs().then(resp => { Sist2AdminApi.getJobs().then(resp => {
this.jobs = resp.data; this._jobs = resp.data;
this.loading = false; this.loading = false;
}); });
}, },
data() { computed: {
return { jobs() {
loading: true, return this._jobs
.filter(job => job.index_options.search_backend === this.frontend.web_options.search_backend)
}
},
data() {
return {
loading: true,
_jobs: null
}
} }
}
} }
</script> </script>

View File

@@ -44,8 +44,7 @@ export default {
return ""; return "";
} }
const date = Date.parse(dateString); return moment.utc(dateString).local().fromNow();
return moment(date).fromNow();
} }
} }
} }

View File

@@ -1,57 +1,94 @@
<template> <template>
<div> <div>
<b-form-checkbox :checked="desktopNotificationsEnabled" @change="updateNotifications($event)"> <b-form-checkbox :checked="desktopNotificationsEnabled" @change="updateNotifications($event)">
{{ $t("jobOptions.desktopNotifications") }} {{ $t("jobOptions.desktopNotifications") }}
</b-form-checkbox> </b-form-checkbox>
<b-form-checkbox v-model="job.schedule_enabled" @change="update()"> <b-form-checkbox v-model="job.schedule_enabled" @change="update()">
{{ $t("jobOptions.scheduleEnabled") }} {{ $t("jobOptions.scheduleEnabled") }}
</b-form-checkbox> </b-form-checkbox>
<label>{{ $t("jobOptions.cron") }}</label> <label>{{ $t("jobOptions.cron") }}</label>
<b-form-input class="text-monospace" :state="cronValid" v-model="job.cron_expression" :disabled="!job.schedule_enabled" @change="update()"></b-form-input> <b-form-input class="text-monospace" :state="cronValid" v-model="job.cron_expression"
</div> :disabled="!job.schedule_enabled" @change="update()"></b-form-input>
<label>{{ $t("jobOptions.keepNLogs") }}</label>
<b-input-group>
<b-form-input type="number" v-model="job.keep_last_n_logs" @change="update()"></b-form-input>
<b-input-group-append>
<b-button variant="danger" @click="onDeleteNowClick()">{{ $t("jobOptions.deleteNow") }}</b-button>
</b-input-group-append>
</b-input-group>
</div>
</template> </template>
<script> <script>
import Sist2AdminApi from "@/Sist2AdminApi";
export default { export default {
name: "JobOptions", name: "JobOptions",
props: ["job"], props: ["job"],
data() { data() {
return { return {
cronValid: undefined cronValid: undefined,
} logsToDelete: null
}, }
computed: { },
desktopNotificationsEnabled() { computed: {
return this.$store.state.jobDesktopNotificationMap[this.job.name]; desktopNotificationsEnabled() {
} return this.$store.state.jobDesktopNotificationMap[this.job.name];
}, }
},
mounted() { mounted() {
this.cronValid = this.checkCron(this.job.cron_expression) this.cronValid = this.checkCron(this.job.cron_expression)
}, },
methods: { methods: {
checkCron(expression) { checkCron(expression) {
return /((((\d+,)+\d+|(\d+([/-])\d+)|\d+|\*) ?){5,7})/.test(expression); return /((((\d+,)+\d+|(\d+([/-])\d+)|\d+|\*) ?){5,7})/.test(expression);
}, },
updateNotifications(value) { updateNotifications(value) {
this.$store.dispatch("setJobDesktopNotification", { this.$store.dispatch("setJobDesktopNotification", {
job: this.job.name, job: this.job.name,
enabled: value enabled: value
}); });
}, },
update() { update() {
if (this.job.schedule_enabled) { if (this.job.schedule_enabled) {
this.cronValid = this.checkCron(this.job.cron_expression); this.cronValid = this.checkCron(this.job.cron_expression);
} else { } else {
this.cronValid = undefined; this.cronValid = undefined;
} }
if (this.cronValid !== false) { if (this.cronValid !== false) {
this.$emit("change", this.job); this.$emit("change", this.job);
} }
},
onDeleteNowClick() {
Sist2AdminApi.getLogsToDelete(this.job.name, this.job.keep_last_n_logs).then(resp => {
const toDelete = resp.data;
const message = `Delete ${toDelete.length} log files?`;
this.$bvModal.msgBoxConfirm(message, {
title: this.$t("confirmation"),
size: "sm",
buttonSize: "sm",
okVariant: "danger",
okTitle: this.$t("delete"),
cancelTitle: this.$t("cancel"),
footerClass: "p-2",
hideHeaderClose: false,
centered: true
}).then(value => {
if (value) {
toDelete.forEach(row => {
Sist2AdminApi.deleteTaskLogs(row["id"]);
});
}
});
})
}
}, },
},
} }
</script> </script>

View File

@@ -0,0 +1,34 @@
<template>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<span v-else-if="jobs.length === 0"></span>
<b-form-select v-else :options="jobs" text-field="name" value-field="name"
@change="$emit('change', $event)" :value="$t('selectJob')"></b-form-select>
</template>
<script>
import Sist2AdminApi from "@/Sist2AdminApi";
export default {
name: "JobSelect",
mounted() {
Sist2AdminApi.getJobs().then(resp => {
this._jobs = resp.data;
this.loading = false;
});
},
computed: {
jobs() {
return [
{name: this.$t("selectJob"), disabled: true},
...this._jobs.filter(job => job.index_path)
]
}
},
data() {
return {
loading: true,
_jobs: null
}
}
}
</script>

View File

@@ -7,7 +7,7 @@
<b-form-input type="number" min="1" v-model="options.threads" @change="update()"></b-form-input> <b-form-input type="number" min="1" v-model="options.threads" @change="update()"></b-form-input>
<label>{{ $t("scanOptions.thumbnailQuality") }}</label> <label>{{ $t("scanOptions.thumbnailQuality") }}</label>
<b-form-input type="number" min="1" max="31" v-model="options.thumbnail_quality" @change="update()"></b-form-input> <b-form-input type="number" min="0" max="100" v-model="options.thumbnail_quality" @change="update()"></b-form-input>
<label>{{ $t("scanOptions.thumbnailCount") }}</label> <label>{{ $t("scanOptions.thumbnailCount") }}</label>
<b-form-input type="number" min="0" max="1000" v-model="options.thumbnail_count" @change="update()"></b-form-input> <b-form-input type="number" min="0" max="1000" v-model="options.thumbnail_count" @change="update()"></b-form-input>

View File

@@ -0,0 +1,24 @@
<template>
<b-list-group-item action :to="`/searchBackend/${backend.name}`">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
{{ backend.name }}
</h5>
<div>
<b-badge v-if="backend.backend_type === 'sqlite'" variant="info">SQLite</b-badge>
<b-badge v-else variant="info">Elasticsearch</b-badge>
</div>
</div>
</b-list-group-item>
</template>
<script>
export default {
name: "SearchBackendListItem",
props: ["backend"],
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<div v-else>
<label>{{$t("backendOptions.searchBackend")}}</label>
<b-select :options="options" :value="value" @change="$emit('change', $event)"></b-select>
</div>
</template>
<script>
import Sist2AdminApi from "@/Sist2AdminApi";
export default {
name: "SearchBackendSelect",
props: ["value"],
data() {
return {
loading: true,
backends: null,
}
},
computed: {
options() {
return this.backends.map(backend => backend.name)
}
},
mounted() {
Sist2AdminApi.getSearchBackends().then(resp => {
this.loading = false;
this.backends = resp.data
})
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,18 @@
<template>
<b-list-group-item action :to="`/userScript/${script.name}`">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
{{ script.name }}
</h5>
</div>
</b-list-group-item>
</template>
<script>
export default {
name: "UserScriptListItem",
props: ["script"],
}
</script>

View File

@@ -0,0 +1,88 @@
<template>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<b-row v-else>
<b-col cols="6">
<h5>Selected scripts</h5>
<b-list-group>
<b-list-group-item v-for="script in selectedScripts" :key="script"
button
@click="onRemoveScript(script)"
class="d-flex justify-content-between align-items-center">
{{ script }}
<b-button-group>
<b-button variant="light" @click.stop="moveUpScript(script)"></b-button>
<b-button variant="light" @click.stop="moveDownScript(script)"></b-button>
</b-button-group>
</b-list-group-item>
</b-list-group>
</b-col>
<b-col cols="6">
<h5>Available scripts</h5>
<b-list-group>
<b-list-group-item v-for="script in availableScripts" :key="script" button
@click="onSelectScript(script)">
{{ script }}
</b-list-group-item>
</b-list-group>
</b-col>
</b-row>
<!-- <b-checkbox-group v-else :options="scripts" stacked :checked="selectedScripts"-->
<!-- @input="$emit('change', $event)"></b-checkbox-group>-->
</template>
<script>
import Sist2AdminApi from "@/Sist2AdminApi";
export default {
name: "UserScriptPicker",
props: ["selectedScripts"],
data() {
return {
loading: true,
scripts: []
}
},
computed: {
availableScripts() {
return this.scripts.filter(script => !this.selectedScripts.includes(script))
}
},
mounted() {
Sist2AdminApi.getUserScripts().then(resp => {
this.scripts = resp.data.map(script => script.name);
this.loading = false;
});
},
methods: {
onSelectScript(name) {
this.selectedScripts.push(name);
this.$emit("change", this.selectedScripts)
},
onRemoveScript(name) {
this.selectedScripts.splice(this.selectedScripts.indexOf(name), 1);
this.$emit("change", this.selectedScripts);
},
moveUpScript(name) {
const index = this.selectedScripts.indexOf(name);
if (index > 0) {
this.selectedScripts.splice(index, 1);
this.selectedScripts.splice(index - 1, 0, name);
}
this.$emit("change", this.selectedScripts);
},
moveDownScript(name) {
const index = this.selectedScripts.indexOf(name);
if (index < this.selectedScripts.length - 1) {
this.selectedScripts.splice(index, 1);
this.selectedScripts.splice(index + 1, 0, name);
}
this.$emit("change", this.selectedScripts);
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,88 +1,59 @@
<template> <template>
<div> <div>
<label>{{ $t("webOptions.esUrl") }}</label> <h4>{{ $t("webOptions.title") }}</h4>
<b-alert :variant="esTestOk ? 'success' : 'danger'" :show="showEsTestAlert" class="mt-1"> <b-card>
{{ esTestMessage }} <label>{{ $t("webOptions.lang") }}</label>
</b-alert> <b-form-select v-model="options.lang" :options="['en', 'fr', 'zh-CN', 'pl', 'de']"
@change="update()"></b-form-select>
<b-input-group> <label>{{ $t("webOptions.bind") }}</label>
<b-form-input v-model="options.es_url" @change="update()"></b-form-input> <b-form-input v-model="options.bind" @change="update()"></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="testEs()">{{ $t("test") }}</b-button>
</b-input-group-append>
</b-input-group>
<b-form-checkbox v-model="options.es_insecure_ssl" :disabled="!this.options.es_url.startsWith('https')" @change="update()"> <label>{{ $t("webOptions.tagline") }}</label>
{{ $t("webOptions.esInsecure") }} <b-form-textarea v-model="options.tagline" @change="update()"></b-form-textarea>
</b-form-checkbox>
<label>{{ $t("webOptions.esIndex") }}</label> <label>{{ $t("webOptions.auth") }}</label>
<b-form-input v-model="options.es_index" @change="update()"></b-form-input> <b-form-input v-model="options.auth" @change="update()"></b-form-input>
<label>{{ $t("webOptions.lang") }}</label> <label>{{ $t("webOptions.tagAuth") }}</label>
<b-form-select v-model="options.lang" :options="['en', 'fr', 'zh-CN']" @change="update()"></b-form-select> <b-form-input v-model="options.tag_auth" @change="update()"></b-form-input>
</b-card>
<label>{{ $t("webOptions.bind") }}</label> <br>
<b-form-input v-model="options.bind" @change="update()"></b-form-input> <h4>Auth0 options</h4>
<b-card>
<label>{{ $t("webOptions.auth0Audience") }}</label>
<b-form-input v-model="options.auth0_audience" @change="update()"></b-form-input>
<label>{{ $t("webOptions.tagline") }}</label> <label>{{ $t("webOptions.auth0Domain") }}</label>
<b-form-textarea v-model="options.tagline" @change="update()"></b-form-textarea> <b-form-input v-model="options.auth0_domain" @change="update()"></b-form-input>
<label>{{ $t("webOptions.auth") }}</label> <label>{{ $t("webOptions.auth0ClientId") }}</label>
<b-form-input v-model="options.auth" @change="update()"></b-form-input> <b-form-input v-model="options.auth0_client_id" @change="update()"></b-form-input>
<label>{{ $t("webOptions.tagAuth") }}</label> <label>{{ $t("webOptions.auth0PublicKey") }}</label>
<b-form-input v-model="options.tag_auth" @change="update()"></b-form-input> <b-textarea rows="10" v-model="options.auth0_public_key" @change="update()"></b-textarea>
</b-card>
<br> </div>
<h5>Auth0 options</h5>
<label>{{ $t("webOptions.auth0Audience") }}</label>
<b-form-input v-model="options.auth0_audience" @change="update()"></b-form-input>
<label>{{ $t("webOptions.auth0Domain") }}</label>
<b-form-input v-model="options.auth0_domain" @change="update()"></b-form-input>
<label>{{ $t("webOptions.auth0ClientId") }}</label>
<b-form-input v-model="options.auth0_client_id" @change="update()"></b-form-input>
<label>{{ $t("webOptions.auth0PublicKey") }}</label>
<b-textarea rows="10" v-model="options.auth0_public_key" @change="update()"></b-textarea>
</div>
</template> </template>
<script> <script>
import sist2AdminApi from "@/Sist2AdminApi";
export default { export default {
name: "WebOptions", name: "WebOptions",
props: ["options", "frontendName"], props: ["options", "frontendName"],
data() { data() {
return { return {
showEsTestAlert: false, showEsTestAlert: false,
esTestOk: false, esTestOk: false,
esTestMessage: "", esTestMessage: "",
} }
},
methods: {
update() {
if (!this.options.es_url.startsWith("https")) {
this.options.es_insecure_ssl = false;
}
this.$emit("change", this.options);
}, },
testEs() { methods: {
sist2AdminApi.pingEs(this.options.es_url, this.options.es_insecure_ssl).then((resp) => { update() {
this.showEsTestAlert = true; this.$emit("change", this.options);
this.esTestOk = resp.data.ok; },
this.esTestMessage = resp.data.message;
});
} }
}
} }
</script> </script>

View File

@@ -5,10 +5,13 @@ export default {
go: "Go", go: "Go",
online: "online", online: "online",
offline: "offline", offline: "offline",
view: "View",
delete: "Delete", delete: "Delete",
runNow: "Index now", runNow: "Index now",
create: "Create", create: "Create",
cancel: "Cancel",
test: "Test", test: "Test",
confirmation: "Confirmation",
jobTitle: "job configuration", jobTitle: "job configuration",
tasks: "Tasks", tasks: "Tasks",
@@ -45,12 +48,26 @@ export default {
extraQueryArgs: "Extra query arguments when launching from sist2-admin", extraQueryArgs: "Extra query arguments when launching from sist2-admin",
customUrl: "Custom URL when launching from sist2-admin", customUrl: "Custom URL when launching from sist2-admin",
selectJobs: "Select jobs", searchBackends: "Search backends",
searchBackendTitle: "search backend configuration",
newBackendName: "New search backend name",
frontendTab: "Frontend",
backendTab: "Backend",
scripts: "User Scripts",
script: "User Script",
testScript: "Test/debug User Script",
newScriptName: "New script name",
scriptType: "Script type",
scriptCode: "Script code (Python)",
scriptOptions: "User scripts",
gitRepository: "Git repository URL",
extraArgs: "Extra command line arguments",
selectJobs: "Available jobs",
selectJob: "Select a job",
webOptions: { webOptions: {
title: "Web options", title: "Web options",
esUrl: "Elasticsearch URL",
esIndex: "Elasticsearch index name",
esInsecure: "Do not verify SSL connections to Elasticsearch.",
lang: "UI Language", lang: "UI Language",
bind: "Listen address", bind: "Listen address",
tagline: "Tagline in navbar", tagline: "Tagline in navbar",
@@ -61,12 +78,24 @@ export default {
auth0ClientId: "Auth0 client ID", auth0ClientId: "Auth0 client ID",
auth0PublicKey: "Auth0 public key", auth0PublicKey: "Auth0 public key",
}, },
backendOptions: {
title: "Search backend options",
searchBackend: "Search backend",
type: "Search backend type",
esUrl: "Elasticsearch URL",
esIndex: "Elasticsearch index name",
esInsecure: "Do not verify SSL connections to Elasticsearch.",
threads: "Number of threads",
batchSize: "Index batch size",
script: "User script",
searchIndex: "Search index file location"
},
scanOptions: { scanOptions: {
title: "Scanning options", title: "Scanning options",
path: "Path", path: "Path",
threads: "Number of threads", threads: "Number of threads",
memThrottle: "Total memory threshold in MiB for scan throttling", memThrottle: "Total memory threshold in MiB for scan throttling",
thumbnailQuality: "Thumbnail quality, on a scale of 2 to 32, 2 being the best", thumbnailQuality: "Thumbnail quality, on a scale of 0 to 100, 100 being the best",
thumbnailCount: "Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails.", thumbnailCount: "Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails.",
thumbnailSize: "Thumbnail size, in pixels", thumbnailSize: "Thumbnail size, in pixels",
contentSize: "Number of bytes to be extracted from text documents. Set to 0 to disable", contentSize: "Number of bytes to be extracted from text documents. Set to 0 to disable",
@@ -87,24 +116,19 @@ export default {
treemapThreshold: "Relative size threshold for treemap", treemapThreshold: "Relative size threshold for treemap",
optimizeIndex: "Defragment index file after scan to reduce its file size." optimizeIndex: "Defragment index file after scan to reduce its file size."
}, },
indexOptions: {
title: "Indexing options",
threads: "Number of threads",
esUrl: "Elasticsearch URL",
esIndex: "Elasticsearch index name",
esInsecure: "Do not verify SSL connections to Elasticsearch.",
batchSize: "Index batch size",
script: "User script"
},
jobOptions: { jobOptions: {
title: "Job options", title: "Job options",
cron: "Job schedule", cron: "Job schedule",
keepNLogs: "Keep last N log files. Set to -1 to keep all logs.",
deleteNow: "Delete now",
scheduleEnabled: "Enable scheduled re-scan", scheduleEnabled: "Enable scheduled re-scan",
noJobAvailable: "No jobs available.", noJobAvailable: "No jobs available for this search backend.",
notIndexed: "Has not been indexed yet",
noBackendError: "You must select a search backend to run this job",
desktopNotifications: "Desktop notifications" desktopNotifications: "Desktop notifications"
}, },
frontendOptions: { frontendOptions: {
title: "Frontend options", title: "Advanced options",
noJobSelectedWarning: "You must select at least one job to start this frontend" noJobSelectedWarning: "You must select at least one job to start this frontend"
}, },
notifications: { notifications: {

View File

@@ -5,12 +5,19 @@ import Job from "@/views/Job";
import Tasks from "@/views/Tasks"; import Tasks from "@/views/Tasks";
import Frontend from "@/views/Frontend"; import Frontend from "@/views/Frontend";
import Tail from "@/views/Tail"; import Tail from "@/views/Tail";
import SearchBackend from "@/views/SearchBackend.vue";
import UserScript from "@/views/UserScript.vue";
Vue.use(VueRouter); Vue.use(VueRouter);
const routes = [ const routes = [
{ {
path: "/", path: "/task",
name: "Tasks",
component: Tasks
},
{
path: "/:tab?",
name: "Home", name: "Home",
component: Home component: Home
}, },
@@ -19,16 +26,21 @@ const routes = [
name: "Job", name: "Job",
component: Job component: Job
}, },
{
path: "/task/",
name: "Tasks",
component: Tasks
},
{ {
path: "/frontend/:name", path: "/frontend/:name",
name: "Frontend", name: "Frontend",
component: Frontend component: Frontend
}, },
{
path: "/searchBackend/:name",
name: "SearchBackend",
component: SearchBackend
},
{
path: "/userScript/:name",
name: "UserScript",
component: UserScript
},
{ {
path: "/log/:taskId", path: "/log/:taskId",
name: "Tail", name: "Tail",

View File

@@ -1,60 +1,63 @@
<template> <template>
<b-card> <b-card>
<b-card-title> <b-card-title>
{{ name }} {{ name }}
<small style="vertical-align: top"> <small style="vertical-align: top">
<b-badge v-if="!loading && frontend.running" variant="success">{{ $t("online") }}</b-badge> <b-badge v-if="!loading && frontend.running" variant="success">{{ $t("online") }}</b-badge>
<b-badge v-else-if="!loading" variant="secondary">{{ $t("offline") }}</b-badge> <b-badge v-else-if="!loading" variant="secondary">{{ $t("offline") }}</b-badge>
</small> </small>
</b-card-title> </b-card-title>
<div class="mb-3" v-if="!loading"> <!-- Action buttons-->
<b-button class="mr-1" :disabled="frontend.running || !valid" variant="success" @click="start()">{{ <div class="mb-3" v-if="!loading">
$t("start") <b-button class="mr-1" :disabled="frontend.running || !valid" variant="success" @click="start()">{{
}} $t("start")
</b-button> }}
<b-button class="mr-1" :disabled="!frontend.running" variant="danger" @click="stop()">{{ </b-button>
$t("stop") <b-button class="mr-1" :disabled="!frontend.running" variant="danger" @click="stop()">{{
}} $t("stop")
</b-button> }}
<b-button class="mr-1" :disabled="!frontend.running" variant="primary" :href="frontendUrl" target="_blank"> </b-button>
{{ $t("go") }} <b-button class="mr-1" :disabled="!frontend.running" variant="primary" :href="frontendUrl" target="_blank">
</b-button> {{ $t("go") }}
<b-button variant="danger" @click="deleteFrontend()">{{ $t("delete") }}</b-button> </b-button>
</div> <b-button variant="danger" @click="deleteFrontend()">{{ $t("delete") }}</b-button>
</div>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card-body v-else>
<b-progress v-if="loading" striped animated value="100"></b-progress> <h4>{{ $t("backendOptions.title") }}</h4>
<b-card-body v-else> <b-card>
<b-alert v-if="!valid" variant="warning" show>{{ $t("frontendOptions.noJobSelectedWarning") }}</b-alert>
<h4>{{ $t("frontendOptions.title") }}</h4> <SearchBackendSelect :value="frontend.web_options.search_backend"
<b-card> @change="onBackendSelect($event)"></SearchBackendSelect>
<b-form-checkbox v-model="frontend.auto_start" @change="update()">
{{ $t("autoStart") }}
</b-form-checkbox>
<label>{{ $t("extraQueryArgs") }}</label> <br>
<b-form-input v-model="frontend.extra_query_args" @change="update()"></b-form-input> <JobCheckboxGroup :frontend="frontend" @input="update()"></JobCheckboxGroup>
</b-card>
<label>{{ $t("customUrl") }}</label> <br/>
<b-form-input v-model="frontend.custom_url" @change="update()" placeholder="http://"></b-form-input>
<br/> <WebOptions :options="frontend.web_options" :frontend-name="$route.params.name"
@change="update()"></WebOptions>
<br/>
<b-alert v-if="!valid" variant="warning" show>{{ $t("frontendOptions.noJobSelectedWarning") }}</b-alert> <h4>{{ $t("frontendOptions.title") }}</h4>
<b-card>
<b-form-checkbox v-model="frontend.auto_start" @change="update()">
{{ $t("autoStart") }}
</b-form-checkbox>
<JobCheckboxGroup :frontend="frontend" @input="update()"></JobCheckboxGroup> <label>{{ $t("extraQueryArgs") }}</label>
</b-card> <b-form-input v-model="frontend.extra_query_args" @change="update()"></b-form-input>
<br/> <label>{{ $t("customUrl") }}</label>
<b-form-input v-model="frontend.custom_url" @change="update()" placeholder="http://"></b-form-input>
<h4>{{ $t("webOptions.title") }}</h4> </b-card>
<b-card> </b-card-body>
<WebOptions :options="frontend.web_options" :frontend-name="$route.params.name" @change="update()"></WebOptions> </b-card>
</b-card>
</b-card-body>
</b-card>
</template> </template>
<script> <script>
@@ -62,68 +65,74 @@
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
import JobCheckboxGroup from "@/components/JobCheckboxGroup"; import JobCheckboxGroup from "@/components/JobCheckboxGroup";
import WebOptions from "@/components/WebOptions"; import WebOptions from "@/components/WebOptions";
import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
export default { export default {
name: 'Frontend', name: 'Frontend',
components: {JobCheckboxGroup, WebOptions}, components: {SearchBackendSelect, JobCheckboxGroup, WebOptions},
data() { data() {
return { return {
loading: true, loading: true,
frontend: null, frontend: null,
} }
},
computed: {
valid() {
return !this.loading && this.frontend.jobs.length > 0;
}, },
frontendUrl() { computed: {
if (this.frontend.custom_url) { valid() {
return this.frontend.custom_url + this.args; return !this.loading && this.frontend.jobs.length > 0;
} },
frontendUrl() {
if (this.frontend.custom_url) {
return this.frontend.custom_url + this.args;
}
if (this.frontend.web_options.bind.startsWith("0.0.0.0")) { if (this.frontend.web_options.bind.startsWith("0.0.0.0")) {
return window.location.protocol + "//" + window.location.hostname + ":" + this.port + this.args; return window.location.protocol + "//" + window.location.hostname + ":" + this.port + this.args;
} }
return window.location.protocol + "//" + this.frontend.web_options.bind + this.args; return window.location.protocol + "//" + this.frontend.web_options.bind + this.args;
},
name() {
return this.$route.params.name;
},
port() {
return this.frontend.web_options.bind.split(":")[1]
},
args() {
const args = this.frontend.extra_query_args;
if (args !== "") {
return "#" + (args.startsWith("?") ? (args) : ("?" + args));
}
return "";
}
}, },
name() { mounted() {
return this.$route.params.name; Sist2AdminApi.getFrontend(this.name).then(resp => {
this.frontend = resp.data;
this.loading = false;
});
}, },
port() { methods: {
return this.frontend.web_options.bind.split(":")[1] start() {
}, this.frontend.running = true;
args() { Sist2AdminApi.startFrontend(this.name)
const args = this.frontend.extra_query_args; },
if (args !== "") { stop() {
return "#" + (args.startsWith("?") ? (args) : ("?" + args)); this.frontend.running = false;
} Sist2AdminApi.stopFrontend(this.name)
return ""; },
deleteFrontend() {
Sist2AdminApi.deleteFrontend(this.name).then(() => {
this.$router.push("/");
});
},
update() {
Sist2AdminApi.updateFrontend(this.name, this.frontend);
},
onBackendSelect(backend) {
this.frontend.web_options.search_backend = backend;
this.frontend.jobs = [];
this.update();
}
} }
},
mounted() {
Sist2AdminApi.getFrontend(this.name).then(resp => {
this.frontend = resp.data;
this.loading = false;
});
},
methods: {
start() {
this.frontend.running = true;
Sist2AdminApi.startFrontend(this.name)
},
stop() {
this.frontend.running = false;
Sist2AdminApi.stopFrontend(this.name)
},
deleteFrontend() {
Sist2AdminApi.deleteFrontend(this.name).then(() => {
this.$router.push("/frontends");
});
},
update() {
Sist2AdminApi.updateFrontend(this.name, this.frontend);
},
}
} }
</script> </script>

View File

@@ -1,60 +1,123 @@
<template> <template>
<div> <div>
<b-card> <b-tabs content-class="mt-3" v-model="tab" @input="onTabChange($event)">
<b-card-title>{{ $t("jobs") }}</b-card-title> <b-tab :title="$t('backendTab')">
<b-row>
<b-col>
<b-input id="new-job" v-model="newJobName" :placeholder="$t('newJobName')"></b-input>
<b-popover
:show.sync="showHelp"
target="new-job"
placement="top"
triggers="manual"
variant="primary"
:content="$t('newJobHelp')"
></b-popover>
</b-col>
<b-col>
<b-button variant="primary" @click="createJob()" :disabled="!jobNameValid(newJobName)">{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<hr/> <b-card>
<b-card-title>{{ $t("searchBackends") }}</b-card-title>
<b-progress v-if="jobsLoading" striped animated value="100"></b-progress> <b-row>
<b-list-group v-else> <b-col>
<JobListItem v-for="job in jobs" :key="job.name" :job="job"></JobListItem> <b-input v-model="newBackendName" :placeholder="$t('newBackendName')"></b-input>
</b-list-group> </b-col>
</b-card> <b-col>
<b-button variant="primary" @click="createBackend()"
:disabled="!backendNameValid(newBackendName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<br/> <hr/>
<b-card> <b-progress v-if="backendsLoading" striped animated value="100"></b-progress>
<b-list-group v-else>
<SearchBackendListItem v-for="backend in backends"
:key="backend.name" :backend="backend"></SearchBackendListItem>
</b-list-group>
</b-card>
<b-card-title>{{ $t("frontends") }}</b-card-title> <br/>
<b-row> <b-card>
<b-col> <b-card-title>{{ $t("jobs") }}</b-card-title>
<b-input v-model="newFrontendName" :placeholder="$t('newFrontendName')"></b-input> <b-row>
</b-col> <b-col>
<b-col> <b-input id="new-job" v-model="newJobName" :placeholder="$t('newJobName')"></b-input>
<b-button variant="primary" @click="createFrontend()" :disabled="!frontendNameValid(newFrontendName)"> <b-popover
{{ $t("create") }} :show.sync="showHelp"
</b-button> target="new-job"
</b-col> placement="top"
</b-row> triggers="manual"
variant="primary"
:content="$t('newJobHelp')"
></b-popover>
</b-col>
<b-col>
<b-button variant="primary" @click="createJob()" :disabled="!jobNameValid(newJobName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<hr/> <hr/>
<b-progress v-if="frontendsLoading" striped animated value="100"></b-progress> <b-progress v-if="jobsLoading" striped animated value="100"></b-progress>
<b-list-group v-else> <b-list-group v-else>
<FrontendListItem v-for="frontend in frontends" <JobListItem v-for="job in jobs" :key="job.name" :job="job"></JobListItem>
:key="frontend.name" :frontend="frontend"></FrontendListItem> </b-list-group>
</b-list-group> </b-card>
</b-tab>
<b-tab :title="$t('scripts')">
</b-card> <b-progress v-if="scriptsLoading" striped animated value="100"></b-progress>
</div> <b-card v-else>
<b-card-title>{{ $t("scripts") }}</b-card-title>
<label>Select template</label>
<b-form-radio-group stacked :options="scriptTemplates" v-model="scriptTemplate"></b-form-radio-group>
<br>
<b-row>
<b-col>
<b-form-input v-model="newScriptName" :disabled="!scriptTemplate" :placeholder="$t('newScriptName')"></b-form-input>
</b-col>
<b-col>
<b-button variant="primary" @click="createScript()"
:disabled="!scriptNameValid(newScriptName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<hr/>
<b-list-group>
<UserScriptListItem v-for="script in scripts"
:key="script.name" :script="script"></UserScriptListItem>
</b-list-group>
</b-card>
</b-tab>
<b-tab :title="$t('frontendTab')">
<b-card>
<b-card-title>{{ $t("frontends") }}</b-card-title>
<b-row>
<b-col>
<b-input v-model="newFrontendName" :placeholder="$t('newFrontendName')"></b-input>
</b-col>
<b-col>
<b-button variant="primary" @click="createFrontend()"
:disabled="!frontendNameValid(newFrontendName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<hr/>
<b-progress v-if="frontendsLoading" striped animated value="100"></b-progress>
<b-list-group v-else>
<FrontendListItem v-for="frontend in frontends"
:key="frontend.name" :frontend="frontend"></FrontendListItem>
</b-list-group>
</b-card>
</b-tab>
</b-tabs>
</div>
</template> </template>
<script> <script>
@@ -62,61 +125,116 @@ import JobListItem from "@/components/JobListItem";
import {formatBindAddress} from "@/util"; import {formatBindAddress} from "@/util";
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
import FrontendListItem from "@/components/FrontendListItem"; import FrontendListItem from "@/components/FrontendListItem";
import SearchBackendListItem from "@/components/SearchBackendListItem.vue";
import UserScriptListItem from "@/components/UserScriptListItem.vue";
export default { export default {
name: "Jobs", name: "Jobs",
components: {JobListItem, FrontendListItem}, components: {UserScriptListItem, SearchBackendListItem, JobListItem, FrontendListItem},
data() { data() {
return { return {
jobsLoading: true, jobsLoading: true,
newJobName: "", newJobName: "",
jobs: [], jobs: [],
frontendsLoading: true, frontendsLoading: true,
frontends: [], frontends: [],
formatBindAddress, formatBindAddress,
newFrontendName: "", newFrontendName: "",
showHelp: false backends: [],
backendsLoading: true,
newBackendName: "",
scripts: [],
scriptTemplates: [],
newScriptName: "",
scriptTemplate: null,
scriptsLoading: true,
showHelp: false,
tab: 0
}
},
mounted() {
this.loading = true;
if (this.$route.params.tab) {
console.log("mounted " + this.$route.params.tab)
window.setTimeout(() => {
this.tab = Math.round(Number(this.$route.params.tab));
}, 1)
}
this.reload();
},
methods: {
jobNameValid(name) {
if (this.jobs.some(job => job.name === name)) {
return false;
}
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
},
frontendNameValid(name) {
if (this.frontends.some(frontend => frontend.name === name)) {
return false;
}
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
},
backendNameValid(name) {
if (this.backends.some(backend => backend.name === name)) {
return false;
}
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
},
scriptNameValid(name) {
if (this.scripts.some(script => script.name === name)) {
return false;
}
if (name.length > 16) {
return false;
}
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
},
reload() {
Sist2AdminApi.getJobs().then(resp => {
this.jobs = resp.data;
this.jobsLoading = false;
this.showHelp = this.jobs.length === 0;
});
Sist2AdminApi.getFrontends().then(resp => {
this.frontends = resp.data;
this.frontendsLoading = false;
});
Sist2AdminApi.getSearchBackends().then(resp => {
this.backends = resp.data;
this.backendsLoading = false;
})
Sist2AdminApi.getUserScripts().then(resp => {
this.scripts = resp.data;
this.scriptTemplates = this.$store.state.sist2AdminInfo.user_script_templates;
this.scriptsLoading = false;
})
},
createJob() {
Sist2AdminApi.createJob(this.newJobName).then(this.reload);
},
createFrontend() {
Sist2AdminApi.createFrontend(this.newFrontendName).then(this.reload)
},
createBackend() {
Sist2AdminApi.createBackend(this.newBackendName).then(this.reload);
},
createScript() {
Sist2AdminApi.createUserScript(this.newScriptName, this.scriptTemplate).then(this.reload)
},
onTabChange(tab) {
if (this.$route.params.tab != tab) {
this.$router.push({params: {tab: tab}})
}
}
} }
},
mounted() {
this.loading = true;
this.reload();
},
methods: {
jobNameValid(name) {
if (this.jobs.some(job => job.name === name)) {
return false;
}
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
},
frontendNameValid(name) {
if (this.frontends.some(job => job.name === name)) {
return false;
}
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
},
reload() {
Sist2AdminApi.getJobs().then(resp => {
this.jobs = resp.data;
this.jobsLoading = false;
this.showHelp = this.jobs.length === 0;
});
Sist2AdminApi.getFrontends().then(resp => {
this.frontends = resp.data;
this.frontendsLoading = false;
});
},
createJob() {
Sist2AdminApi.createJob(this.newJobName).then(this.reload);
},
createFrontend() {
Sist2AdminApi.createFrontend(this.newFrontendName).then(this.reload)
}
}
} }
</script> </script>

View File

@@ -1,92 +1,125 @@
<template> <template>
<b-card> <b-card>
<b-card-title> <b-card-title>
[{{ getName() }}] [{{ getName() }}]
{{ $t("jobTitle") }} {{ $t("jobTitle") }}
</b-card-title> </b-card-title>
<div class="mb-3"> <div class="mb-3">
<b-button class="mr-1" variant="primary" @click="runJob()">{{ $t("runNow") }}</b-button> <b-button class="mr-1" variant="primary" @click="runJob()" :disabled="!valid">{{ $t("runNow") }}</b-button>
<b-button variant="danger" @click="deleteJob()">{{ $t("delete") }}</b-button> <b-button variant="danger" @click="deleteJob()">{{ $t("delete") }}</b-button>
</div> </div>
<div v-if="job"> <div v-if="job">
{{ $t("status") }}: <code>{{ job.status }}</code> {{ $t("status") }}: <code>{{ job.status }}</code>
</div> </div>
<b-progress v-if="loading" striped animated value="100"></b-progress> <b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card-body v-else> <b-card-body v-else>
<h4>{{ $t("jobOptions.title") }}</h4> <h4>{{ $t("jobOptions.title") }}</h4>
<b-card> <b-card>
<JobOptions :job="job" @change="update"></JobOptions> <JobOptions :job="job" @change="update"></JobOptions>
</b-card> </b-card>
<br/> <br/>
<h4>{{ $t("scanOptions.title") }}</h4> <h4>{{ $t("backendOptions.title") }}</h4>
<b-card> <b-card>
<ScanOptions :options="job.scan_options" @change="update()"></ScanOptions> <b-alert v-if="!valid" variant="warning" show>{{ $t("jobOptions.noBackendError") }}</b-alert>
</b-card> <SearchBackendSelect :value="job.index_options.search_backend"
@change="onBackendSelect($event)"></SearchBackendSelect>
</b-card>
<br/>
<br/> <h4>{{ $t("scriptOptions") }}</h4>
<b-card>
<UserScriptPicker :selected-scripts="job.user_scripts"
@change="onScriptChange($event)"></UserScriptPicker>
</b-card>
<h4>{{ $t("indexOptions.title") }}</h4> <br/>
<b-card>
<IndexOptions :options="job.index_options" @change="update()"></IndexOptions>
</b-card>
</b-card-body> <h4>{{ $t("scanOptions.title") }}</h4>
<b-card>
<ScanOptions :options="job.scan_options" @change="update()"></ScanOptions>
</b-card>
</b-card> </b-card-body>
</b-card>
</template> </template>
<script> <script>
import ScanOptions from "@/components/ScanOptions"; import ScanOptions from "@/components/ScanOptions";
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
import IndexOptions from "@/components/IndexOptions";
import JobOptions from "@/components/JobOptions"; import JobOptions from "@/components/JobOptions";
import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
import UserScriptPicker from "@/components/UserScriptPicker.vue";
export default { export default {
name: "Job", name: "Job",
components: { components: {
IndexOptions, UserScriptPicker,
ScanOptions, SearchBackendSelect,
JobOptions ScanOptions,
}, JobOptions
data() { },
return { data() {
loading: true, return {
job: null loading: true,
job: null,
}
},
methods: {
getName() {
return this.$route.params.name;
},
update() {
Sist2AdminApi.updateJob(this.getName(), this.job);
},
runJob() {
Sist2AdminApi.runJob(this.getName()).then(() => {
this.$bvToast.toast(this.$t("runJobConfirmation"), {
title: this.$t("runJobConfirmationTitle"),
variant: "success",
toaster: "b-toaster-bottom-right"
});
});
},
deleteJob() {
Sist2AdminApi.deleteJob(this.getName())
.then(() => {
this.$router.push("/");
})
.catch(err => {
this.$bvToast.toast("Cannot delete job " +
"because it is referenced by a frontend", {
title: "Error",
variant: "danger",
toaster: "b-toaster-bottom-right"
});
})
},
onBackendSelect(backend) {
this.job.index_options.search_backend = backend;
this.update();
},
onScriptChange(scripts) {
this.job.user_scripts = scripts;
this.update();
}
},
mounted() {
Sist2AdminApi.getJob(this.getName()).then(resp => {
this.loading = false;
this.job = resp.data;
})
},
computed: {
valid() {
return this.job?.index_options.search_backend != null;
}
} }
},
methods: {
getName() {
return this.$route.params.name;
},
update() {
Sist2AdminApi.updateJob(this.getName(), this.job);
},
runJob() {
Sist2AdminApi.runJob(this.getName()).then(() => {
this.$bvToast.toast(this.$t("runJobConfirmation"), {
title: this.$t("runJobConfirmationTitle"),
variant: "success",
toaster: "b-toaster-bottom-right"
});
});
},
deleteJob() {
Sist2AdminApi.deleteJob(this.getName()).then(() => {
this.$router.push("/");
})
}
},
mounted() {
Sist2AdminApi.getJob(this.getName()).then(resp => {
this.loading = false;
this.job = resp.data;
})
}
} }
</script> </script>

View File

@@ -0,0 +1,123 @@
<template>
<b-card>
<b-card-title>
<span class="text-monospace">{{ getName() }}</span>
{{ $t("searchBackendTitle") }}
</b-card-title>
<div class="mb-3">
<b-button variant="danger" @click="deleteBackend()">{{ $t("delete") }}</b-button>
</div>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card-body v-else>
<label>{{ $t("backendOptions.type") }}</label>
<b-select :options="backendTypeOptions" v-model="backend.backend_type" @change="update()"></b-select>
<hr/>
<template v-if="backend.backend_type === 'elasticsearch'">
<b-alert :variant="esTestOk ? 'success' : 'danger'" :show="showEsTestAlert" class="mt-1">
{{ esTestMessage }}
</b-alert>
<label>{{ $t("backendOptions.esUrl") }}</label>
<b-input-group>
<b-form-input v-model="backend.es_url" @change="update()"></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="testEs()">{{ $t("test") }}</b-button>
</b-input-group-append>
</b-input-group>
<b-form-checkbox v-model="backend.es_insecure_ssl" :disabled="!this.backend.es_url.startsWith('https')"
@change="update()">
{{ $t("backendOptions.esInsecure") }}
</b-form-checkbox>
<label>{{ $t("backendOptions.esIndex") }}</label>
<b-form-input v-model="backend.es_index" @change="update()"></b-form-input>
<label>{{ $t("backendOptions.threads") }}</label>
<b-form-input v-model="backend.threads" type="number" min="1" @change="update()"></b-form-input>
<label>{{ $t("backendOptions.batchSize") }}</label>
<b-form-input v-model="backend.batch_size" type="number" min="1" @change="update()"></b-form-input>
</template>
<template v-else>
<label>{{ $t("backendOptions.searchIndex") }}</label>
<b-form-input v-model="backend.search_index" disabled></b-form-input>
</template>
</b-card-body>
</b-card>
</template>
<script>
import sist2AdminApi from "@/Sist2AdminApi";
import Sist2AdminApi from "@/Sist2AdminApi";
export default {
name: "SearchBackend",
data() {
return {
showEsTestAlert: false,
esTestOk: false,
esTestMessage: "",
loading: true,
backend: null,
backendTypeOptions: [
{
text: "Elasticsearch",
value: "elasticsearch"
},
{
text: "SQLite",
value: "sqlite"
}
]
}
},
mounted() {
Sist2AdminApi.getSearchBackend(this.getName()).then(resp => {
this.backend = resp.data;
this.loading = false;
});
},
methods: {
getName() {
return this.$route.params.name;
},
testEs() {
sist2AdminApi.pingEs(this.backend.es_url, this.backend.es_insecure_ssl)
.then((resp) => {
this.showEsTestAlert = true;
this.esTestOk = resp.data.ok;
this.esTestMessage = resp.data.message;
});
},
update() {
Sist2AdminApi.updateSearchBackend(this.getName(), this.backend);
},
deleteBackend() {
Sist2AdminApi.deleteBackend(this.getName())
.then(() => {
this.$router.push("/");
})
.catch(err => {
this.$bvToast.toast("Cannot delete search backend " +
"because it is referenced by a job or frontend", {
title: "Error",
variant: "danger",
toaster: "b-toaster-bottom-right"
});
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,168 +1,175 @@
<template> <template>
<b-card> <b-card>
<b-card-body> <b-card-body>
<h4 class="mb-3">{{ taskId }} {{ $t("logs") }}</h4> <h4 class="mb-3">{{ taskId }} {{ $t("logs") }}</h4>
<div v-if="$store.state.sist2AdminInfo"> <div v-if="$store.state.sist2AdminInfo">
{{ $t("logFile") }} {{ $t("logFile") }}
<code>{{ $store.state.sist2AdminInfo.logs_folder }}/sist2-{{ taskId }}.log</code> <code>{{ $store.state.sist2AdminInfo.logs_folder }}/sist2-{{ taskId }}.log</code>
<br/> <br/>
<br/> <br/>
</div> </div>
<b-row> <b-row>
<b-col> <b-col>
<span>{{ $t("logLevel") }}</span> <span>{{ $t("logLevel") }}</span>
<b-select :options="levels.slice(0, -1)" v-model="logLevel" @input="connect()"></b-select> <b-select :options="levels.slice(0, -1)" v-model="logLevel" @input="connect()"></b-select>
</b-col> </b-col>
<b-col> <b-col>
<span>{{ $t("logMode") }}</span> <span>{{ $t("logMode") }}</span>
<b-select :options="modeOptions" v-model="mode" @input="connect()"></b-select> <b-select :options="modeOptions" v-model="mode" @input="connect()"></b-select>
</b-col> </b-col>
</b-row> </b-row>
<div id="log-tail-output" class="mt-3 ml-1"></div> <div id="log-tail-output" class="mt-3 ml-1"></div>
</b-card-body> </b-card-body>
</b-card> </b-card>
</template> </template>
<script> <script>
export default { export default {
name: "Tail", name: "Tail",
data() { data() {
return { return {
logLevel: "DEBUG", logLevel: "DEBUG",
levels: ["DEBUG", "INFO", "WARNING", "ERROR", "ADMIN", "FATAL"], levels: ["DEBUG", "INFO", "WARNING", "ERROR", "ADMIN", "FATAL"],
socket: null, socket: null,
mode: "follow", mode: "follow",
modeOptions: [ modeOptions: [
{ {
"text": this.$t('follow'), "text": this.$t('follow'),
"value": "follow" "value": "follow"
}, },
{ {
"text": this.$t('wholeFile'), "text": this.$t('wholeFile'),
"value": "wholeFile" "value": "wholeFile"
}
]
} }
] },
computed: {
taskId: function () {
return this.$route.params.taskId;
}
},
methods: {
connect() {
let lineCount = 0;
const outputElem = document.getElementById("log-tail-output")
outputElem.replaceChildren();
if (this.socket !== null) {
this.socket.close();
}
const n = this.mode === "follow" ? 32 : 9999999999;
if (window.location.protocol === "https:") {
this.socket = new WebSocket(`wss://${window.location.host}/log/${this.taskId}?n=${n}`);
} else {
this.socket = new WebSocket(`ws://${window.location.host}/log/${this.taskId}?n=${n}`);
}
this.socket.onopen = () => {
this.socket.send("Hello from client");
}
this.socket.onmessage = e => {
let message;
try {
message = JSON.parse(e.data);
} catch {
console.error(e.data)
return;
}
if ("ping" in message) {
return;
}
if (message.level === undefined) {
if ("stderr" in message) {
message.level = "ERROR";
message.message = message["stderr"];
} else if ("stdout" in message) {
message.level = "INFO";
message.message = message["stdout"];
} else {
message.level = "ADMIN";
message.message = message["sist2-admin"];
}
message.datetime = ""
message.filepath = ""
}
if (this.levels.indexOf(message.level) < this.levels.indexOf(this.logLevel)) {
return;
}
const logLine = `${message.datetime} [${message.level} ${message.filepath}] ${message.message}`;
const span = document.createElement("span");
span.setAttribute("class", message.level);
span.appendChild(document.createTextNode(logLine));
outputElem.appendChild(span);
lineCount += 1;
if (this.mode === "follow" && lineCount >= n) {
outputElem.firstChild.remove();
}
}
}
},
mounted() {
this.connect()
} }
},
computed: {
taskId: function () {
return this.$route.params.taskId;
}
},
methods: {
connect() {
let lineCount = 0;
const outputElem = document.getElementById("log-tail-output")
outputElem.replaceChildren();
if (this.socket !== null) {
this.socket.close();
}
const n = this.mode === "follow" ? 32 : 9999999999;
this.socket = new WebSocket(`ws://${window.location.host}/log/${this.taskId}?n=${n}`);
this.socket.onopen = () => {
this.socket.send("Hello from client");
}
this.socket.onmessage = e => {
let message;
try {
message = JSON.parse(e.data);
} catch {
console.error(e.data)
return;
}
if ("ping" in message) {
return;
}
if (message.level === undefined) {
if ("stderr" in message) {
message.level = "ERROR";
message.message = message["stderr"];
} else {
message.level = "ADMIN";
message.message = message["sist2-admin"];
}
message.datetime = ""
message.filepath = ""
}
if (this.levels.indexOf(message.level) < this.levels.indexOf(this.logLevel)) {
return;
}
const logLine = `${message.datetime} [${message.level} ${message.filepath}] ${message.message}`;
const span = document.createElement("span");
span.setAttribute("class", message.level);
span.appendChild(document.createTextNode(logLine));
outputElem.appendChild(span);
lineCount += 1;
if (this.mode === "follow" && lineCount >= n) {
outputElem.firstChild.remove();
}
}
}
},
mounted() {
this.connect()
}
} }
</script> </script>
<style> <style>
#log-tail-output span { #log-tail-output span {
display: block; display: block;
} }
span.DEBUG { span.DEBUG {
color: #9E9E9E; color: #9E9E9E;
} }
span.WARNING { span.WARNING {
color: #FFB300; color: #FFB300;
} }
span.INFO { span.INFO {
color: #039BE5; color: #039BE5;
} }
span.ERROR { span.ERROR {
color: #F4511E; color: #F4511E;
} }
span.FATAL { span.FATAL {
color: #F4511E; color: #F4511E;
} }
span.ADMIN { span.ADMIN {
color: #ee05ff; color: #ee05ff;
} }
#log-tail-output { #log-tail-output {
font-size: 13px; font-size: 13px;
font-family: monospace; font-family: monospace;
padding: 6px; padding: 6px;
background-color: #f5f5f5; background-color: #f5f5f5;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
margin: 3px; margin: 3px;
white-space: pre; white-space: pre;
color: #000; color: #000;
overflow: hidden; overflow: hidden;
} }
</style> </style>

View File

@@ -1,38 +1,49 @@
<template> <template>
<div> <div>
<b-card v-if="tasks.length > 0"> <b-card v-if="tasks.length > 0">
<h2>{{ $t("runningTasks") }}</h2> <h2>{{ $t("runningTasks") }}</h2>
<b-list-group> <b-list-group>
<TaskListItem v-for="task in tasks" :key="task.id" :task="task"></TaskListItem> <TaskListItem v-for="task in tasks" :key="task.id" :task="task"></TaskListItem>
</b-list-group> </b-list-group>
</b-card> </b-card>
<b-card class="mt-4"> <b-card class="mt-4">
<b-card-title>{{ $t("taskHistory") }}</b-card-title> <b-card-title>{{ $t("taskHistory") }}</b-card-title>
<br/> <br/>
<b-table <b-table
id="task-history" id="task-history"
:items="historyItems" :items="historyItems"
:fields="historyFields" :fields="historyFields"
:current-page="historyCurrentPage" :current-page="historyCurrentPage"
:tbody-tr-class="rowClass" :tbody-tr-class="rowClass"
:per-page="10" :per-page="10"
> >
<template #cell(logs)="data"> <template #cell(logs)="data">
<router-link :to="`/log/${data.item.logs}`">{{ $t("logs") }}</router-link> <template v-if="data.item._row.has_logs">
</template> <b-button variant="link" size="sm" :to="`/log/${data.item.id}`">
{{ $t("view") }}
</b-button>
/
<b-button variant="link" size="sm" @click="deleteLogs(data.item.id)">
{{ $t("delete") }}
</b-button>
</template>
</template>
</b-table> <template #cell(delete)="data">
</template>
<b-pagination limit="20" v-model="historyCurrentPage" :total-rows="historyItems.length" </b-table>
:per-page="10"></b-pagination>
</b-card> <b-pagination limit="20" v-model="historyCurrentPage" :total-rows="historyItems.length"
</div> :per-page="10"></b-pagination>
</b-card>
</div>
</template> </template>
<script> <script>
@@ -45,106 +56,116 @@ const HOUR = 3600;
const MINUTE = 60; const MINUTE = 60;
function humanDuration(sec_num) { function humanDuration(sec_num) {
sec_num = sec_num / 1000; sec_num = sec_num / 1000;
const days = Math.floor(sec_num / DAY); const days = Math.floor(sec_num / DAY);
sec_num -= days * DAY; sec_num -= days * DAY;
const hours = Math.floor(sec_num / HOUR); const hours = Math.floor(sec_num / HOUR);
sec_num -= hours * HOUR; sec_num -= hours * HOUR;
const minutes = Math.floor(sec_num / MINUTE); const minutes = Math.floor(sec_num / MINUTE);
sec_num -= minutes * MINUTE; sec_num -= minutes * MINUTE;
const seconds = Math.floor(sec_num); const seconds = Math.floor(sec_num);
if (days > 0) { if (days > 0) {
return `${days} days ${hours}h ${minutes}m ${seconds}s`; return `${days} days ${hours}h ${minutes}m ${seconds}s`;
} }
if (hours > 0) { if (hours > 0) {
return `${hours}h ${minutes}m ${seconds}s`; return `${hours}h ${minutes}m ${seconds}s`;
} }
if (minutes > 0) { if (minutes > 0) {
return `${minutes}m ${seconds}s`; return `${minutes}m ${seconds}s`;
} }
if (seconds > 0) { if (seconds > 0) {
return `${seconds}s`; return `${seconds}s`;
} }
return "<0s"; return "<0s";
} }
export default { export default {
name: 'Tasks', name: 'Tasks',
components: {TaskListItem}, components: {TaskListItem},
data() { data() {
return { return {
loading: true, loading: true,
tasks: [], tasks: [],
taskHistory: [], taskHistory: [],
timerId: null, timerId: null,
historyFields: [ historyFields: [
{key: "name", label: this.$t("taskName")}, {key: "name", label: this.$t("taskName")},
{key: "time", label: this.$t("taskStarted")}, {key: "time", label: this.$t("taskStarted")},
{key: "duration", label: this.$t("taskDuration")}, {key: "duration", label: this.$t("taskDuration")},
{key: "status", label: this.$t("taskStatus")}, {key: "status", label: this.$t("taskStatus")},
{key: "logs", label: this.$t("logs")}, {key: "logs", label: this.$t("logs")},
], ],
historyCurrentPage: 1, historyCurrentPage: 1,
historyItems: [] historyItems: []
} }
}, },
props: { props: {
msg: String msg: String
}, },
mounted() { mounted() {
this.loading = true; this.loading = true;
this.update().then(() => this.loading = false); this.update().then(() => this.loading = false);
this.timerId = window.setInterval(this.update, 1000); this.timerId = window.setInterval(this.update, 1000);
this.updateHistory(); this.updateHistory();
},
destroyed() {
if (this.timerId) {
window.clearInterval(this.timerId);
}
},
methods: {
rowClass(row) {
if (row.status === "failed") {
return "table-danger";
}
return null;
}, },
updateHistory() { destroyed() {
Sist2AdminApi.getTaskHistory().then(resp => { if (this.timerId) {
this.historyItems = resp.data.map(row => ({ window.clearInterval(this.timerId);
id: row.id, }
name: row.name,
duration: this.taskDuration(row),
time: moment(row.started).format("dd, MMM Do YYYY, HH:mm:ss"),
logs: row.id,
status: row.return_code === 0 ? "ok" : "failed"
}));
});
}, },
update() { methods: {
return Sist2AdminApi.getTasks().then(resp => { rowClass(row) {
this.tasks = resp.data; if (row.status === "failed") {
}) return "table-danger";
}, }
taskDuration(task) { return null;
const start = moment.utc(task.started); },
const end = moment.utc(task.ended); updateHistory() {
Sist2AdminApi.getTaskHistory().then(resp => {
this.historyItems = resp.data.map(row => ({
id: row.id,
name: row.name,
duration: this.taskDuration(row),
time: moment.utc(row.started).local().format("dd, MMM Do YYYY, HH:mm:ss"),
logs: null,
status: [0,1].includes(row.return_code) ? "ok" : "failed",
_row: row
}));
});
},
update() {
return Sist2AdminApi.getTasks().then(resp => {
this.tasks = resp.data;
})
},
taskDuration(task) {
const start = moment.utc(task.started);
const end = moment.utc(task.ended);
return humanDuration(end.diff(start)) return humanDuration(end.diff(start))
},
deleteLogs(taskId) {
Sist2AdminApi.deleteTaskLogs(taskId).then(() => {
this.updateHistory();
})
}
} }
}
} }
</script> </script>
<style scoped> <style scoped>
#task-history { #task-history {
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
}
.btn-link {
padding: 0;
} }
</style> </style>

View File

@@ -0,0 +1,117 @@
<template>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card v-else>
<b-card-title>
{{ $route.params.name }}
{{ $t("script") }}
</b-card-title>
<div class="mb-3">
<b-button variant="danger" @click="deleteScript()">{{ $t("delete") }}</b-button>
</div>
<b-card>
<h5>{{ $t("testScript") }}</h5>
<b-row>
<b-col cols="11">
<JobSelect @change="onJobSelect($event)"></JobSelect>
</b-col>
<b-col cols="1">
<b-button :disabled="!selectedTestJob" variant="primary" @click="testScript()">{{ $t("test") }}
</b-button>
</b-col>
</b-row>
</b-card>
<br/>
<label>{{ $t("scriptType") }}</label>
<b-form-select :options="['git', 'simple']" v-model="script.type" @change="update()"></b-form-select>
<template v-if="script.type === 'git'">
<label>{{ $t("gitRepository") }}</label>
<b-form-input v-model="script.git_repository" placeholder="https://github.com/example/example.git"
@change="update()"></b-form-input>
<label>{{ $t("extraArgs") }}</label>
<b-form-input v-model="script.extra_args" @change="update()" class="text-monospace"></b-form-input>
</template>
<template v-if="script.type === 'simple'">
<label>{{ $t("scriptCode") }}</label>
<p>Find sist2-python documentation <a href="https://sist2-python.readthedocs.io/" target="_blank">here</a></p>
<b-textarea rows="15" class="text-monospace" v-model="script.script" @change="update()" spellcheck="false"></b-textarea>
</template>
<template v-if="script.type === 'local'">
<!-- TODO-->
</template>
</b-card>
</template>
<script>
import Sist2AdminApi from "@/Sist2AdminApi";
import JobOptions from "@/components/JobOptions.vue";
import JobCheckboxGroup from "@/components/JobCheckboxGroup.vue";
import JobSelect from "@/components/JobSelect.vue";
export default {
name: "UserScript",
components: {JobSelect, JobCheckboxGroup, JobOptions},
data() {
return {
loading: true,
script: null,
selectedTestJob: null
}
},
methods: {
update() {
Sist2AdminApi.updateUserScript(this.name, this.script);
},
onJobSelect(job) {
this.selectedTestJob = job;
},
deleteScript() {
Sist2AdminApi.deleteUserScript(this.name)
.then(() => {
this.$router.push("/");
})
.catch(err => {
this.$bvToast.toast("Cannot delete user script " +
"because it is referenced by a job", {
title: "Error",
variant: "danger",
toaster: "b-toaster-bottom-right"
});
})
},
testScript() {
Sist2AdminApi.testUserScript(this.name, this.selectedTestJob)
.then(() => {
this.$bvToast.toast(this.$t("runJobConfirmation"), {
title: this.$t("runJobConfirmationTitle"),
variant: "success",
toaster: "b-toaster-bottom-right"
});
})
}
},
mounted() {
Sist2AdminApi.getUserScript(this.name).then(resp => {
this.script = resp.data;
this.loading = false;
});
},
computed: {
name() {
return this.$route.params.name;
},
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,6 @@ fastapi
git+https://github.com/simon987/hexlib.git git+https://github.com/simon987/hexlib.git
uvicorn uvicorn
websockets websockets
pycron pycron
GitPython
git+https://github.com/simon987/sist2-python.git

View File

@@ -18,11 +18,13 @@ from websockets.exceptions import ConnectionClosed
import cron import cron
from config import LOG_FOLDER, logger, WEBSERVER_PORT, DATA_FOLDER, SIST2_BINARY from config import LOG_FOLDER, logger, WEBSERVER_PORT, DATA_FOLDER, SIST2_BINARY
from jobs import Sist2Job, Sist2ScanTask, TaskQueue, Sist2IndexTask, JobStatus from jobs import Sist2Job, Sist2ScanTask, TaskQueue, Sist2IndexTask, JobStatus, Sist2UserScriptTask
from notifications import Subscribe, Notifications from notifications import Subscribe, Notifications
from sist2 import Sist2 from sist2 import Sist2, Sist2SearchBackend
from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION, migrate_v3_to_v4, \
get_log_files_to_remove, delete_log_file, create_default_search_backends
from web import Sist2Frontend from web import Sist2Frontend
from script import UserScript, SCRIPT_TEMPLATES
sist2 = Sist2(SIST2_BINARY, DATA_FOLDER) sist2 = Sist2(SIST2_BINARY, DATA_FOLDER)
db = PersistentState(dbfile=os.path.join(DATA_FOLDER, "state.db")) db = PersistentState(dbfile=os.path.join(DATA_FOLDER, "state.db"))
@@ -51,7 +53,8 @@ async def home():
async def api(): async def api():
return { return {
"tesseract_langs": TESSERACT_LANGS, "tesseract_langs": TESSERACT_LANGS,
"logs_folder": LOG_FOLDER "logs_folder": LOG_FOLDER,
"user_script_templates": list(SCRIPT_TEMPLATES.keys())
} }
@@ -73,16 +76,14 @@ async def get_frontend(name: str):
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@app.get("/api/job/") @app.get("/api/job")
async def get_jobs(): async def get_jobs():
return list(db["jobs"]) return list(db["jobs"])
@app.put("/api/job/{name:str}") @app.put("/api/job/{name:str}")
async def update_job(name: str, new_job: Sist2Job): async def update_job(name: str, new_job: Sist2Job):
# TODO: Check etag new_job.last_modified = datetime.utcnow()
new_job.last_modified = datetime.now()
job = db["jobs"][name] job = db["jobs"][name]
if not job: if not job:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@@ -114,12 +115,10 @@ async def update_job(name: str, new_job: Sist2Job):
async def update_frontend(name: str, frontend: Sist2Frontend): async def update_frontend(name: str, frontend: Sist2Frontend):
db["frontends"][name] = frontend db["frontends"][name] = frontend
# TODO: Check etag
return "ok" return "ok"
@app.get("/api/task/") @app.get("/api/task")
async def get_tasks(): async def get_tasks():
return list(map(lambda t: t.json(), task_queue.tasks())) return list(map(lambda t: t.json(), task_queue.tasks()))
@@ -134,16 +133,38 @@ async def kill_job(task_id: str):
return task_queue.kill_task(task_id) return task_queue.kill_task(task_id)
@app.post("/api/task/{task_id:str}/delete_logs")
async def delete_task_logs(task_id: str):
if not db["task_done"][task_id]:
raise HTTPException(status_code=404)
delete_log_file(db, task_id)
return "ok"
def _run_job(job: Sist2Job): def _run_job(job: Sist2Job):
job.last_modified = datetime.now() job.last_modified = datetime.utcnow()
if job.status == JobStatus("created"): if job.status == JobStatus("created"):
job.status = JobStatus("started") job.status = JobStatus("started")
db["jobs"][job.name] = job db["jobs"][job.name] = job
scan_task = Sist2ScanTask(job, f"Scan [{job.name}]") scan_task = Sist2ScanTask(job, f"Scan [{job.name}]")
index_task = Sist2IndexTask(job, f"Index [{job.name}]", depends_on=scan_task)
index_depends_on = scan_task
script_tasks = []
for script_name in job.user_scripts:
script = db["user_scripts"][script_name]
task = Sist2UserScriptTask(script, job, f"Script <{script_name}> [{job.name}]", depends_on=scan_task)
script_tasks.append(task)
index_depends_on = task
index_task = Sist2IndexTask(job, f"Index [{job.name}]", depends_on=index_depends_on)
task_queue.submit(scan_task) task_queue.submit(scan_task)
for task in script_tasks:
task_queue.submit(task)
task_queue.submit(index_task) task_queue.submit(index_task)
@@ -158,14 +179,45 @@ async def run_job(name: str):
return "ok" return "ok"
@app.get("/api/user_script/{name:str}/run")
def run_user_script(name: str, job: str):
script = db["user_scripts"][name]
if not script:
raise HTTPException(status_code=404)
job = db["jobs"][job]
if not job:
raise HTTPException(status_code=404)
script_task = Sist2UserScriptTask(script, job, f"Script <{name}> [{job.name}]")
task_queue.submit(script_task)
return "ok"
@app.get("/api/job/{name:str}/logs_to_delete")
async def task_history(n: int, name: str):
return get_log_files_to_remove(db, name, n)
@app.delete("/api/job/{name:str}") @app.delete("/api/job/{name:str}")
async def delete_job(name: str): async def delete_job(name: str):
job = db["jobs"][name] job: Sist2Job = db["jobs"][name]
if job: if not job:
del db["jobs"][name]
else:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
if any(name in frontend.jobs for frontend in db["frontends"]):
raise HTTPException(status_code=400, detail="in use (frontend)")
try:
os.remove(job.previous_index)
except:
pass
del db["jobs"][name]
return "ok"
@app.delete("/api/frontend/{name:str}") @app.delete("/api/frontend/{name:str}")
async def delete_frontend(name: str): async def delete_frontend(name: str):
@@ -215,7 +267,7 @@ def check_es_version(es_url: str, insecure: bool):
es_url = f"{url.scheme}://{url.hostname}:{url.port}" es_url = f"{url.scheme}://{url.hostname}:{url.port}"
else: else:
auth = None auth = None
r = requests.get(es_url, verify=insecure, auth=auth) r = requests.get(es_url, verify=not insecure, auth=auth)
except SSLError: except SSLError:
return { return {
"ok": False, "ok": False,
@@ -251,9 +303,21 @@ def check_es_version(es_url: str, insecure: bool):
def start_frontend_(frontend: Sist2Frontend): def start_frontend_(frontend: Sist2Frontend):
frontend.web_options.indices = list(map(lambda j: db["jobs"][j].index_path, frontend.jobs)) frontend.web_options.indices = [
os.path.join(DATA_FOLDER, db["jobs"][j].index_path)
for j in frontend.jobs
]
pid = sist2.web(frontend.web_options, frontend.name) backend_name = frontend.web_options.search_backend
search_backend = db["search_backends"][backend_name]
if search_backend is None:
logger.error(
f"Error while running task: search backend not found: {backend_name}")
return -1
logger.debug(f"Fetched search backend options for {backend_name}")
pid = sist2.web(frontend.web_options, search_backend, frontend.name)
RUNNING_FRONTENDS[frontend.name] = pid RUNNING_FRONTENDS[frontend.name] = pid
@@ -273,7 +337,7 @@ async def stop_frontend(name: str):
del RUNNING_FRONTENDS[name] del RUNNING_FRONTENDS[name]
@app.get("/api/frontend/") @app.get("/api/frontend")
async def get_frontends(): async def get_frontends():
res = [] res = []
for frontend in db["frontends"]: for frontend in db["frontends"]:
@@ -283,6 +347,115 @@ async def get_frontends():
return res return res
@app.get("/api/search_backend")
async def get_search_backends():
return list(db["search_backends"])
@app.put("/api/search_backend/{name:str}")
async def update_search_backend(name: str, backend: Sist2SearchBackend):
if not db["search_backends"][name]:
raise HTTPException(status_code=404)
db["search_backends"][name] = backend
return "ok"
@app.get("/api/search_backend/{name:str}")
def get_search_backend(name: str):
backend = db["search_backends"][name]
if not backend:
raise HTTPException(status_code=404)
return backend
@app.delete("/api/search_backend/{name:str}")
def delete_search_backend(name: str):
backend: Sist2SearchBackend = db["search_backends"][name]
if not backend:
raise HTTPException(status_code=404)
if any(frontend.web_options.search_backend == name for frontend in db["frontends"]):
raise HTTPException(status_code=400, detail="in use (frontend)")
if any(job.index_options.search_backend == name for job in db["jobs"]):
raise HTTPException(status_code=400, detail="in use (job)")
del db["search_backends"][name]
try:
os.remove(os.path.join(DATA_FOLDER, backend.search_index))
except:
pass
return "ok"
@app.post("/api/search_backend/{name:str}")
def create_search_backend(name: str):
if db["search_backends"][name] is not None:
return HTTPException(status_code=400, detail="already exists")
backend = Sist2SearchBackend.create_default(name)
db["search_backends"][name] = backend
return backend
@app.delete("/api/user_script/{name:str}")
def delete_user_script(name: str):
if db["user_scripts"][name] is None:
return HTTPException(status_code=404)
if any(name in job.user_scripts for job in db["jobs"]):
raise HTTPException(status_code=400, detail="in use (job)")
script: UserScript = db["user_scripts"][name]
script.delete_dir()
del db["user_scripts"][name]
return "ok"
@app.post("/api/user_script/{name:str}")
def create_user_script(name: str, template: str):
if db["user_scripts"][name] is not None:
return HTTPException(status_code=400, detail="already exists")
script = SCRIPT_TEMPLATES[template](name)
db["user_scripts"][name] = script
return script
@app.get("/api/user_script")
async def get_user_scripts():
return list(db["user_scripts"])
@app.get("/api/user_script/{name:str}")
async def get_user_script(name: str):
backend = db["user_scripts"][name]
if not backend:
raise HTTPException(status_code=404)
return backend
@app.put("/api/user_script/{name:str}")
async def update_user_script(name: str, script: UserScript):
previous_version: UserScript = db["user_scripts"][name]
if previous_version and previous_version.git_repository != script.git_repository:
script.force_clone = True
db["user_scripts"][name] = script
return "ok"
def tail(filepath: str, n: int): def tail(filepath: str, n: int):
with open(filepath) as file: with open(filepath) as file:
@@ -321,7 +494,6 @@ async def ws_tail_log(websocket: WebSocket):
async with Subscribe(notifications) as ob: async with Subscribe(notifications) as ob:
async for notification in ob.notifications(): async for notification in ob.notifications():
await websocket.send_json(notification) await websocket.send_json(notification)
print(notification)
except ConnectionClosed: except ConnectionClosed:
return return
@@ -352,7 +524,7 @@ async def ws_tail_log(websocket: WebSocket, task_id: str, n: int):
def main(): def main():
uvicorn.run(app, port=WEBSERVER_PORT, host="0.0.0.0") uvicorn.run(app, port=WEBSERVER_PORT, host="0.0.0.0", timeout_graceful_shutdown=0)
def initialize_db(): def initialize_db():
@@ -361,6 +533,8 @@ def initialize_db():
frontend = Sist2Frontend.create_default("default") frontend = Sist2Frontend.create_default("default")
db["frontends"]["default"] = frontend db["frontends"]["default"] = frontend
create_default_search_backends(db)
logger.info("Initialized database.") logger.info("Initialized database.")
@@ -381,6 +555,13 @@ if __name__ == '__main__':
if db["sist2_admin"]["info"]["version"] == "2": if db["sist2_admin"]["info"]["version"] == "2":
logger.error("Cannot migrate database from v2 to v3. Delete state.db to proceed.") logger.error("Cannot migrate database from v2 to v3. Delete state.db to proceed.")
exit(-1) exit(-1)
if db["sist2_admin"]["info"]["version"] == "3":
logger.info("Migrating to v4 database schema")
migrate_v3_to_v4(db)
if db["sist2_admin"]["info"]["version"] != DB_SCHEMA_VERSION:
raise Exception(f"Incompatible database {db.dbfile}. "
f"Automatic migration is not available, please delete the database file to continue.")
start_frontends() start_frontends()
cron.initialize(db, _run_job) cron.initialize(db, _run_job)

View File

@@ -9,9 +9,11 @@ MAX_LOG_SIZE = 1 * 1024 * 1024
SIST2_BINARY = os.environ.get("SIST2_BINARY", "/root/sist2") SIST2_BINARY = os.environ.get("SIST2_BINARY", "/root/sist2")
DATA_FOLDER = os.environ.get("DATA_FOLDER", "/sist2-admin/") DATA_FOLDER = os.environ.get("DATA_FOLDER", "/sist2-admin/")
LOG_FOLDER = os.path.join(DATA_FOLDER, "logs") LOG_FOLDER = os.path.join(DATA_FOLDER, "logs")
SCRIPT_FOLDER = os.path.join(DATA_FOLDER, "scripts")
WEBSERVER_PORT = 8080 WEBSERVER_PORT = 8080
os.makedirs(LOG_FOLDER, exist_ok=True) os.makedirs(LOG_FOLDER, exist_ok=True)
os.makedirs(SCRIPT_FOLDER, exist_ok=True)
os.makedirs(DATA_FOLDER, exist_ok=True) os.makedirs(DATA_FOLDER, exist_ok=True)
logger = logging.Logger("sist2-admin") logger = logging.Logger("sist2-admin")

View File

@@ -10,7 +10,9 @@ from jobs import Sist2Job
def _check_schedule(db: PersistentState, run_job): def _check_schedule(db: PersistentState, run_job):
for job in db["jobs"]: jobs = list(db["jobs"])
for job in jobs:
job: Sist2Job job: Sist2Job
if job.schedule_enabled: if job.schedule_enabled:

View File

@@ -1,23 +1,29 @@
import json import json
import logging import logging
import os.path import os.path
import shlex
import signal import signal
import uuid import uuid
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from io import TextIOWrapper
from logging import FileHandler from logging import FileHandler
from subprocess import Popen
import subprocess
from threading import Lock, Thread from threading import Lock, Thread
from time import sleep from time import sleep
from typing import List
from uuid import uuid4, UUID from uuid import uuid4, UUID
from hexlib.db import PersistentState from hexlib.db import PersistentState
from pydantic import BaseModel from pydantic import BaseModel
from config import logger, LOG_FOLDER from config import logger, LOG_FOLDER, DATA_FOLDER
from notifications import Notifications from notifications import Notifications
from sist2 import ScanOptions, IndexOptions, Sist2 from sist2 import ScanOptions, IndexOptions, Sist2
from state import RUNNING_FRONTENDS from state import RUNNING_FRONTENDS, get_log_files_to_remove, delete_log_file
from web import Sist2Frontend from web import Sist2Frontend
from script import UserScript
class JobStatus(Enum): class JobStatus(Enum):
@@ -32,9 +38,13 @@ class Sist2Job(BaseModel):
scan_options: ScanOptions scan_options: ScanOptions
index_options: IndexOptions index_options: IndexOptions
user_scripts: List[str] = []
cron_expression: str cron_expression: str
schedule_enabled: bool = False schedule_enabled: bool = False
keep_last_n_logs: int = -1
previous_index: str = None previous_index: str = None
index_path: str = None index_path: str = None
previous_index_path: str = None previous_index_path: str = None
@@ -53,15 +63,10 @@ class Sist2Job(BaseModel):
name=name, name=name,
scan_options=ScanOptions(path="/"), scan_options=ScanOptions(path="/"),
index_options=IndexOptions(), index_options=IndexOptions(),
last_modified=datetime.now(), last_modified=datetime.utcnow(),
cron_expression="0 0 * * *" cron_expression="0 0 * * *"
) )
# @validator("etag", always=True)
# def validate_etag(cls, value, values):
# s = values["name"] + values["scan_options"].json() + values["index_options"].json() + values["cron_expression"]
# return md5(s.encode()).hexdigest()
class Sist2TaskProgress: class Sist2TaskProgress:
@@ -111,7 +116,7 @@ class Sist2Task:
self._logger.info(json.dumps(log_json)) self._logger.info(json.dumps(log_json))
def run(self, sist2: Sist2, db: PersistentState): def run(self, sist2: Sist2, db: PersistentState):
self.started = datetime.now() self.started = datetime.utcnow()
logger.info(f"Started task {self.display_name}") logger.info(f"Started task {self.display_name}")
@@ -132,14 +137,16 @@ class Sist2ScanTask(Sist2Task):
self.pid = pid self.pid = pid
return_code = sist2.scan(self.job.scan_options, logs_cb=self.log_callback, set_pid_cb=set_pid) return_code = sist2.scan(self.job.scan_options, logs_cb=self.log_callback, set_pid_cb=set_pid)
self.ended = datetime.now() self.ended = datetime.utcnow()
if return_code != 0: is_ok = return_code in (0, 1)
if not is_ok:
self._logger.error(json.dumps({"sist2-admin": f"Process returned non-zero exit code ({return_code})"})) self._logger.error(json.dumps({"sist2-admin": f"Process returned non-zero exit code ({return_code})"}))
logger.info(f"Task {self.display_name} failed ({return_code})") logger.info(f"Task {self.display_name} failed ({return_code})")
else: else:
self.job.index_path = self.job.scan_options.output self.job.index_path = self.job.scan_options.output
self.job.last_index_date = datetime.now() self.job.last_index_date = datetime.utcnow()
self.job.do_full_scan = False self.job.do_full_scan = False
db["jobs"][self.job.name] = self.job db["jobs"][self.job.name] = self.job
self._logger.info(json.dumps({"sist2-admin": f"Save last_index_date={self.job.last_index_date}"})) self._logger.info(json.dumps({"sist2-admin": f"Save last_index_date={self.job.last_index_date}"}))
@@ -147,7 +154,7 @@ class Sist2ScanTask(Sist2Task):
logger.info(f"Completed {self.display_name} ({return_code=})") logger.info(f"Completed {self.display_name} ({return_code=})")
# Remove old index # Remove old index
if return_code == 0: if is_ok:
if self.job.previous_index_path is not None and self.job.previous_index_path != self.job.index_path: if self.job.previous_index_path is not None and self.job.previous_index_path != self.job.index_path:
self._logger.info(json.dumps({"sist2-admin": f"Remove {self.job.previous_index_path=}"})) self._logger.info(json.dumps({"sist2-admin": f"Remove {self.job.previous_index_path=}"}))
try: try:
@@ -171,12 +178,19 @@ class Sist2IndexTask(Sist2Task):
self.job.index_options.path = self.job.scan_options.output self.job.index_options.path = self.job.scan_options.output
return_code = sist2.index(self.job.index_options, logs_cb=self.log_callback) search_backend = db["search_backends"][self.job.index_options.search_backend]
self.ended = datetime.now() if search_backend is None:
logger.error(f"Error while running task: search backend not found: {self.job.index_options.search_backend}")
return -1
logger.debug(f"Fetched search backend options for {self.job.index_options.search_backend}")
return_code = sist2.index(self.job.index_options, search_backend, logs_cb=self.log_callback)
self.ended = datetime.utcnow()
duration = self.ended - self.started duration = self.ended - self.started
ok = return_code == 0 ok = return_code in (0, 1)
if ok: if ok:
self.restart_running_frontends(db, sist2) self.restart_running_frontends(db, sist2)
@@ -206,14 +220,84 @@ class Sist2IndexTask(Sist2Task):
except ChildProcessError: except ChildProcessError:
pass pass
frontend.web_options.indices = map(lambda j: db["jobs"][j].index_path, frontend.jobs) backend_name = frontend.web_options.search_backend
search_backend = db["search_backends"][backend_name]
if search_backend is None:
logger.error(f"Error while running task: search backend not found: {backend_name}")
return -1
pid = sist2.web(frontend.web_options, frontend.name) logger.debug(f"Fetched search backend options for {backend_name}")
frontend.web_options.indices = [
os.path.join(DATA_FOLDER, db["jobs"][j].index_path)
for j in frontend.jobs
]
pid = sist2.web(frontend.web_options, search_backend, frontend.name)
RUNNING_FRONTENDS[frontend_name] = pid RUNNING_FRONTENDS[frontend_name] = pid
self._logger.info(json.dumps({"sist2-admin": f"Restart frontend {pid=} {frontend_name=}"})) self._logger.info(json.dumps({"sist2-admin": f"Restart frontend {pid=} {frontend_name=}"}))
class Sist2UserScriptTask(Sist2Task):
def __init__(self, user_script: UserScript, job: Sist2Job, display_name: str, depends_on: Sist2Task = None):
super().__init__(job, display_name, depends_on=depends_on.id if depends_on else None)
self.user_script = user_script
def run(self, sist2: Sist2, db: PersistentState):
super().run(sist2, db)
try:
self.user_script.setup(self.log_callback)
except Exception as e:
logger.error(f"Setup for {self.user_script.name} failed: ")
logger.exception(e)
self.log_callback({"sist2-admin": f"Setup for {self.user_script.name} failed: {e}"})
return -1
executable = self.user_script.get_executable()
index_path = os.path.join(DATA_FOLDER, self.job.index_path)
extra_args = self.user_script.extra_args
args = [
executable,
index_path,
*shlex.split(extra_args)
]
self.log_callback({"sist2-admin": f"Starting user script with {executable=}, {index_path=}, {extra_args=}"})
proc = Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.user_script.script_dir())
self.pid = proc.pid
t_stderr = Thread(target=self._consume_logs, args=(self.log_callback, proc, "stderr", False))
t_stderr.start()
self._consume_logs(self.log_callback, proc, "stdout", True)
self.ended = datetime.utcnow()
return 0
@staticmethod
def _consume_logs(logs_cb, proc, stream, wait):
pipe_wrapper = TextIOWrapper(getattr(proc, stream), encoding="utf8", errors="ignore")
try:
for line in pipe_wrapper:
if line.strip() == "":
continue
if line.startswith("$PROGRESS"):
progress = json.loads(line[len("$PROGRESS "):])
logs_cb({"progress": progress})
continue
logs_cb({stream: line})
finally:
if wait:
proc.wait()
pipe_wrapper.close()
class TaskQueue: class TaskQueue:
def __init__(self, sist2: Sist2, db: PersistentState, notifications: Notifications): def __init__(self, sist2: Sist2, db: PersistentState, notifications: Notifications):
self._lock = Lock() self._lock = Lock()
@@ -232,7 +316,7 @@ class TaskQueue:
def _tasks_failed(self): def _tasks_failed(self):
done = set() done = set()
for row in self._db["task_done"].sql("WHERE return_code != 0"): for row in self._db["task_done"].sql("WHERE return_code NOT IN (0,1)"):
done.add(uuid.UUID(row["id"])) done.add(uuid.UUID(row["id"]))
return done return done
@@ -301,8 +385,14 @@ class TaskQueue:
"ended": task.ended, "ended": task.ended,
"started": task.started, "started": task.started,
"name": task.display_name, "name": task.display_name,
"return_code": task_result "return_code": task_result,
"has_logs": 1
} }
logs_to_delete = get_log_files_to_remove(self._db, task.job.name, task.job.keep_last_n_logs)
for row in logs_to_delete:
delete_log_file(self._db, row["id"])
if isinstance(task, Sist2IndexTask): if isinstance(task, Sist2IndexTask):
self._notifications.notify({ self._notifications.notify({
"message": "notifications.indexCompleted", "message": "notifications.indexCompleted",

View File

@@ -0,0 +1,126 @@
import os
import shutil
import stat
import subprocess
from enum import Enum
from git import Repo
from pydantic import BaseModel
from config import SCRIPT_FOLDER
class ScriptType(Enum):
LOCAL = "local"
SIMPLE = "simple"
GIT = "git"
def set_executable(file):
os.chmod(file, os.stat(file).st_mode | stat.S_IEXEC)
def _initialize_git_repository(url, path, log_cb, force_clone):
log_cb({"sist2-admin": f"Cloning {url}"})
if force_clone or not os.path.exists(os.path.join(path, ".git")):
if force_clone:
shutil.rmtree(path, ignore_errors=True)
Repo.clone_from(url, path)
else:
repo = Repo(path)
repo.remote("origin").pull()
setup_script = os.path.join(path, "setup.sh")
if setup_script:
log_cb({"sist2-admin": f"Executing setup script {setup_script}"})
set_executable(setup_script)
result = subprocess.run([setup_script], cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
for line in result.stdout.split(b"\n"):
if line:
log_cb({"stdout": line.decode()})
log_cb({"stdout": f"Executed setup script {setup_script}, return code = {result.returncode}"})
if result.returncode != 0:
raise Exception("Error when running setup script!")
log_cb({"sist2-admin": f"Initialized git repository in {path}"})
class UserScript(BaseModel):
name: str
type: ScriptType
git_repository: str = None
force_clone: bool = False
script: str = None
extra_args: str = ""
def script_dir(self):
return os.path.join(SCRIPT_FOLDER, self.name)
def setup(self, log_cb):
os.makedirs(self.script_dir(), exist_ok=True)
if self.type == ScriptType.GIT:
_initialize_git_repository(self.git_repository, self.script_dir(), log_cb, self.force_clone)
self.force_clone = False
elif self.type == ScriptType.SIMPLE:
self._setup_simple()
set_executable(self.get_executable())
def _setup_simple(self):
with open(self.get_executable(), "w") as f:
f.write(
"#!/bin/bash\n"
"python run.py \"$@\""
)
with open(os.path.join(self.script_dir(), "run.py"), "w") as f:
f.write(self.script)
def get_executable(self):
return os.path.join(self.script_dir(), "run.sh")
def delete_dir(self):
shutil.rmtree(self.script_dir(), ignore_errors=True)
SCRIPT_TEMPLATES = {
"CLIP - Generate embeddings to predict the most relevant image based on the text prompt": lambda name: UserScript(
name=name,
type=ScriptType.GIT,
git_repository="https://github.com/simon987/sist2-script-clip",
extra_args="--num-tags=1 --tags-file=general.txt --color=#dcd7ff"
),
"Whisper - Speech to text with OpenAI Whisper": lambda name: UserScript(
name=name,
type=ScriptType.GIT,
git_repository="https://github.com/simon987/sist2-script-whisper",
extra_args="--model=base --num-threads=4 --color=#51da4c --tag"
),
"Hamburger - Simple script example": lambda name: UserScript(
name=name,
type=ScriptType.SIMPLE,
script=
'from sist2 import Sist2Index\n'
'import sys\n'
'\n'
'index = Sist2Index(sys.argv[1])\n'
'for doc in index.document_iter():\n'
' doc.json_data["tag"] = ["hamburger.#00FF00"]\n'
' index.update_document(doc)\n'
'\n'
'index.sync_tag_table()\n'
'index.commit()\n'
'\n'
'print("Done!")\n'
),
"(Blank)": lambda name: UserScript(
name=name,
type=ScriptType.SIMPLE,
script=""
)
}

View File

@@ -3,6 +3,7 @@ import json
import logging import logging
import os.path import os.path
from datetime import datetime from datetime import datetime
from enum import Enum
from io import TextIOWrapper from io import TextIOWrapper
from logging import FileHandler from logging import FileHandler
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
@@ -12,7 +13,7 @@ from typing import List
from pydantic import BaseModel from pydantic import BaseModel
from config import logger, LOG_FOLDER from config import logger, LOG_FOLDER, DATA_FOLDER
class Sist2Version: class Sist2Version:
@@ -25,77 +26,56 @@ class Sist2Version:
return f"{self.major}.{self.minor}.{self.patch}" return f"{self.major}.{self.minor}.{self.patch}"
class WebOptions(BaseModel): class SearchBackendType(Enum):
indices: List[str] = [] SQLITE = "sqlite"
ELASTICSEARCH = "elasticsearch"
class Sist2SearchBackend(BaseModel):
backend_type: SearchBackendType = SearchBackendType("elasticsearch")
name: str
search_index: str = ""
es_url: str = "http://elasticsearch:9200" es_url: str = "http://elasticsearch:9200"
es_insecure_ssl: bool = False es_insecure_ssl: bool = False
es_index: str = "sist2" es_index: str = "sist2"
bind: str = "0.0.0.0:4090" threads: int = 1
auth: str = None batch_size: int = 70
tag_auth: str = None
tagline: str = "Lightning-fast file system indexer and search tool"
dev: bool = False
lang: str = "en"
auth0_audience: str = None
auth0_domain: str = None
auth0_client_id: str = None
auth0_public_key: str = None
auth0_public_key_file: str = None
def __init__(self, **kwargs): @staticmethod
super().__init__(**kwargs) def create_default(name: str, backend_type: SearchBackendType = SearchBackendType("elasticsearch")):
return Sist2SearchBackend(
def args(self): name=name,
args = ["web", f"--es-url={self.es_url}", f"--bind={self.bind}", search_index=f"search-index-{name.replace('/', '_')}.sist2",
f"--tagline={self.tagline}", f"--lang={self.lang}"] backend_type=backend_type
)
if self.auth0_audience:
args.append(f"--auth0-audience={self.auth0_audience}")
if self.auth0_domain:
args.append(f"--auth0-domain={self.auth0_domain}")
if self.auth0_client_id:
args.append(f"--auth0-client-id={self.auth0_client_id}")
if self.auth0_public_key_file:
args.append(f"--auth0-public-key-file={self.auth0_public_key_file}")
if self.es_insecure_ssl:
args.append(f"--es-insecure-ssl")
if self.auth:
args.append(f"--auth={self.auth}")
if self.tag_auth:
args.append(f"--tag-auth={self.tag_auth}")
if self.dev:
args.append(f"--dev")
args.extend(self.indices)
return args
class IndexOptions(BaseModel): class IndexOptions(BaseModel):
path: str = None path: str = None
threads: int = 1
es_url: str = "http://elasticsearch:9200"
es_insecure_ssl: bool = False
es_index: str = "sist2"
incremental_index: bool = True incremental_index: bool = True
script: str = "" search_backend: str = None
script_file: str = None
batch_size: int = 70
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
def args(self): def args(self, search_backend):
absolute_path = os.path.join(DATA_FOLDER, self.path)
args = ["index", self.path, f"--threads={self.threads}", f"--es-url={self.es_url}", if search_backend.backend_type == SearchBackendType("sqlite"):
f"--es-index={self.es_index}", f"--batch-size={self.batch_size}"] search_index_absolute = os.path.join(DATA_FOLDER, search_backend.search_index)
args = ["sqlite-index", absolute_path, "--search-index", search_index_absolute]
else:
args = ["index", absolute_path, f"--threads={search_backend.threads}",
f"--es-url={search_backend.es_url}",
f"--es-index={search_backend.es_index}",
f"--batch-size={search_backend.batch_size}"]
if self.script_file: if search_backend.es_insecure_ssl:
args.append(f"--script-file={self.script_file}") args.append(f"--es-insecure-ssl")
if self.es_insecure_ssl: if self.incremental_index:
args.append(f"--es-insecure-ssl") args.append(f"--incremental-index")
if self.incremental_index:
args.append(f"--incremental-index")
return args return args
@@ -109,7 +89,7 @@ ARCHIVE_RECURSE = "recurse"
class ScanOptions(BaseModel): class ScanOptions(BaseModel):
path: str path: str
threads: int = 1 threads: int = 1
thumbnail_quality: int = 2 thumbnail_quality: int = 50
thumbnail_size: int = 552 thumbnail_size: int = 552
thumbnail_count: int = 1 thumbnail_count: int = 1
content_size: int = 32768 content_size: int = 32768
@@ -137,9 +117,12 @@ class ScanOptions(BaseModel):
super().__init__(**kwargs) super().__init__(**kwargs)
def args(self): def args(self):
output_path = os.path.join(DATA_FOLDER, self.output)
args = ["scan", self.path, f"--threads={self.threads}", f"--thumbnail-quality={self.thumbnail_quality}", args = ["scan", self.path, f"--threads={self.threads}", f"--thumbnail-quality={self.thumbnail_quality}",
f"--thumbnail-count={self.thumbnail_count}", f"--thumbnail-size={self.thumbnail_size}", f"--thumbnail-count={self.thumbnail_count}", f"--thumbnail-size={self.thumbnail_size}",
f"--content-size={self.content_size}", f"--output={self.output}", f"--depth={self.depth}", f"--content-size={self.content_size}", f"--output={output_path}", f"--depth={self.depth}",
f"--archive={self.archive}", f"--mem-buffer={self.mem_buffer}"] f"--archive={self.archive}", f"--mem-buffer={self.mem_buffer}"]
if self.incremental: if self.incremental:
@@ -201,27 +184,75 @@ class Sist2Index:
return self._descriptor["name"] return self._descriptor["name"]
class WebOptions(BaseModel):
indices: List[str] = []
search_backend: str = "elasticsearch"
bind: str = "0.0.0.0:4090"
auth: str = None
tag_auth: str = None
tagline: str = "Lightning-fast file system indexer and search tool"
dev: bool = False
lang: str = "en"
auth0_audience: str = None
auth0_domain: str = None
auth0_client_id: str = None
auth0_public_key: str = None
auth0_public_key_file: str = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
def args(self, search_backend: Sist2SearchBackend):
args = ["web", f"--bind={self.bind}", f"--tagline={self.tagline}",
f"--lang={self.lang}"]
if search_backend.backend_type == SearchBackendType("sqlite"):
search_index_absolute = os.path.join(DATA_FOLDER, search_backend.search_index)
args.append(f"--search-index={search_index_absolute}")
else:
args.append(f"--es-url={search_backend.es_url}")
args.append(f"--es-index={search_backend.es_index}")
if search_backend.es_insecure_ssl:
args.append(f"--es-insecure-ssl")
if self.auth0_audience:
args.append(f"--auth0-audience={self.auth0_audience}")
if self.auth0_domain:
args.append(f"--auth0-domain={self.auth0_domain}")
if self.auth0_client_id:
args.append(f"--auth0-client-id={self.auth0_client_id}")
if self.auth0_public_key_file:
args.append(f"--auth0-public-key-file={self.auth0_public_key_file}")
if self.auth:
args.append(f"--auth={self.auth}")
if self.tag_auth:
args.append(f"--tag-auth={self.tag_auth}")
if self.dev:
args.append(f"--dev")
args.extend(self.indices)
return args
class Sist2: class Sist2:
def __init__(self, bin_path: str, data_directory: str): def __init__(self, bin_path: str, data_directory: str):
self._bin_path = bin_path self.bin_path = bin_path
self._data_dir = data_directory self._data_dir = data_directory
def index(self, options: IndexOptions, logs_cb): def index(self, options: IndexOptions, search_backend: Sist2SearchBackend, logs_cb):
if options.script:
with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".painless", delete=False) as f:
f.write(options.script)
options.script_file = f.name
else:
options.script_file = None
args = [ args = [
self._bin_path, self.bin_path,
*options.args(), *options.args(search_backend),
"--json-logs", "--json-logs",
"--very-verbose" "--very-verbose"
] ]
logs_cb({"sist2-admin": f"Starting sist2 command with args {args}"})
proc = Popen(args, stdout=PIPE, stderr=PIPE) proc = Popen(args, stdout=PIPE, stderr=PIPE)
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc)) t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
@@ -236,13 +267,10 @@ class Sist2:
def scan(self, options: ScanOptions, logs_cb, set_pid_cb): def scan(self, options: ScanOptions, logs_cb, set_pid_cb):
if options.output is None: if options.output is None:
options.output = os.path.join( options.output = f"scan-{options.name.replace('/', '_')}-{datetime.utcnow()}.sist2"
self._data_dir,
f"scan-{options.name.replace('/', '_')}-{datetime.now()}.sist2"
)
args = [ args = [
self._bin_path, self.bin_path,
*options.args(), *options.args(),
"--json-logs", "--json-logs",
"--very-verbose" "--very-verbose"
@@ -290,7 +318,7 @@ class Sist2:
except NameError: except NameError:
pass pass
def web(self, options: WebOptions, name: str): def web(self, options: WebOptions, search_backend: Sist2SearchBackend, name: str):
if options.auth0_public_key: if options.auth0_public_key:
with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".txt", delete=False) as f: with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".txt", delete=False) as f:
@@ -300,8 +328,8 @@ class Sist2:
options.auth0_public_key_file = None options.auth0_public_key_file = None
args = [ args = [
self._bin_path, self.bin_path,
*options.args() *options.args(search_backend)
] ]
web_logger = logging.Logger(name=f"sist2-frontend-{name}") web_logger = logging.Logger(name=f"sist2-frontend-{name}")

View File

@@ -1,16 +1,20 @@
from typing import Dict from typing import Dict
import os
import shutil import shutil
from hexlib.db import Table, PersistentState from hexlib.db import Table, PersistentState
import pickle import pickle
from tesseract import get_tesseract_langs from tesseract import get_tesseract_langs
import sqlite3
from config import LOG_FOLDER, logger
from sist2 import SearchBackendType, Sist2SearchBackend
RUNNING_FRONTENDS: Dict[str, int] = {} RUNNING_FRONTENDS: Dict[str, int] = {}
TESSERACT_LANGS = get_tesseract_langs() TESSERACT_LANGS = get_tesseract_langs()
DB_SCHEMA_VERSION = "3" DB_SCHEMA_VERSION = "5"
from pydantic import BaseModel from pydantic import BaseModel
@@ -50,8 +54,35 @@ class PickleTable(Table):
yield dict((k, _deserialize(v)) for k, v in row.items()) yield dict((k, _deserialize(v)) for k, v in row.items())
def migrate_v1_to_v2(db: PersistentState): def get_log_files_to_remove(db: PersistentState, job_name: str, n: int):
if n < 0:
return []
counter = 0
to_remove = []
for row in db["task_done"].sql("WHERE has_logs=1 ORDER BY started DESC"):
if row["name"].endswith(f"[{job_name}]"):
counter += 1
if counter > n:
to_remove.append(row)
return to_remove
def delete_log_file(db: PersistentState, task_id: str):
db["task_done"][task_id] = {
"has_logs": 0
}
try:
os.remove(os.path.join(LOG_FOLDER, f"sist2-{task_id}.log"))
except:
pass
def migrate_v1_to_v2(db: PersistentState):
shutil.copy(db.dbfile, db.dbfile + "-before-migrate-v2.bak") shutil.copy(db.dbfile, db.dbfile + "-before-migrate-v2.bak")
# Frontends # Frontends
@@ -77,3 +108,29 @@ def migrate_v1_to_v2(db: PersistentState):
db["sist2_admin"]["info"] = { db["sist2_admin"]["info"] = {
"version": "2" "version": "2"
} }
def create_default_search_backends(db: PersistentState):
es_backend = Sist2SearchBackend.create_default(name="elasticsearch",
backend_type=SearchBackendType("elasticsearch"))
db["search_backends"]["elasticsearch"] = es_backend
sqlite_backend = Sist2SearchBackend.create_default(name="sqlite", backend_type=SearchBackendType("sqlite"))
db["search_backends"]["sqlite"] = sqlite_backend
def migrate_v3_to_v4(db: PersistentState):
shutil.copy(db.dbfile, db.dbfile + "-before-migrate-v4.bak")
create_default_search_backends(db)
try:
conn = sqlite3.connect(db.dbfile)
conn.execute("ALTER TABLE task_done ADD COLUMN has_logs INTEGER DEFAULT 1")
conn.commit()
conn.close()
except Exception as e:
logger.exception(e)
db["sist2_admin"]["info"] = {
"version": "4"
}

View File

@@ -1,12 +1,12 @@
{ {
"name": "sist2", "name": "sist2",
"version": "2.11.0", "version": "1.0.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sist2", "name": "sist2",
"version": "2.11.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@auth0/auth0-spa-js": "^2.0.2", "@auth0/auth0-spa-js": "^2.0.2",
"@egjs/vue-infinitegrid": "3.3.0", "@egjs/vue-infinitegrid": "3.3.0",
@@ -18,6 +18,7 @@
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"fslightbox-vue": "fslightbox-vue.tgz", "fslightbox-vue": "fslightbox-vue.tgz",
"nouislider": "^15.2.0", "nouislider": "^15.2.0",
"onnxruntime-web": "^1.15.1",
"underscore": "^1.13.1", "underscore": "^1.13.1",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-color": "^2.8.1", "vue-color": "^2.8.1",
@@ -29,6 +30,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@types/underscore": "^1.11.6",
"@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8", "@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-typescript": "^5.0.8", "@vue/cli-plugin-typescript": "^5.0.8",
@@ -43,8 +45,8 @@
"portal-vue": "^2.1.7", "portal-vue": "^2.1.7",
"sass": "^1.26.11", "sass": "^1.26.11",
"sass-loader": "^10.0.2", "sass-loader": "^10.0.2",
"typescript": "~4.1.5", "typescript": "^4.9.5",
"vue-cli-plugin-bootstrap-vue": "~0.7.0", "vue-cli-plugin-bootstrap-vue": "~0.8.2",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
} }
}, },
@@ -1955,6 +1957,60 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true "dev": true
}, },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@sideway/address": { "node_modules/@sideway/address": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
@@ -2190,6 +2246,11 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true "dev": true
}, },
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
@@ -2205,8 +2266,7 @@
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.11.18", "version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
"dev": true
}, },
"node_modules/@types/normalize-package-data": { "node_modules/@types/normalize-package-data": {
"version": "2.4.1", "version": "2.4.1",
@@ -2266,6 +2326,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/underscore": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.6.tgz",
"integrity": "sha512-G2oC64I/sR817KDL2b2Mc7+diXyxcibyUeLMyexU4K/sG8hyt/YMlbBK0TVhx/YQ1ehfzgXhLuq2YQHIL4bXUQ==",
"dev": true
},
"node_modules/@types/webpack-env": { "node_modules/@types/webpack-env": {
"version": "1.16.0", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.0.tgz", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.0.tgz",
@@ -2386,9 +2452,9 @@
} }
}, },
"node_modules/@vue/babel-preset-app/node_modules/semver": { "node_modules/@vue/babel-preset-app/node_modules/semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -2721,9 +2787,9 @@
} }
}, },
"node_modules/@vue/cli-plugin-typescript/node_modules/semver": { "node_modules/@vue/cli-plugin-typescript/node_modules/semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -3235,9 +3301,9 @@
} }
}, },
"node_modules/@vue/cli-service/node_modules/semver": { "node_modules/@vue/cli-service/node_modules/semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -3376,9 +3442,9 @@
} }
}, },
"node_modules/@vue/cli-shared-utils/node_modules/semver": { "node_modules/@vue/cli-shared-utils/node_modules/semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -4303,9 +4369,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001444", "version": "1.0.30001515",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001444.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz",
"integrity": "sha512-ecER9xgJQVMqcrxThKptsW0pPxSae8R2RB87LNa+ivW9ppNWRHEplXcDzkCOP4LYWGj8hunXLqaiC41iBATNyg==", "integrity": "sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4315,6 +4381,10 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite" "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
] ]
}, },
@@ -4551,42 +4621,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/cli-highlight/node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/cli-highlight/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cli-highlight/node_modules/yargs-parser": {
"version": "20.2.7",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz",
"integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/cli-spinners": { "node_modules/cli-spinners": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz",
@@ -4876,9 +4910,14 @@
"dev": true "dev": true
}, },
"node_modules/core-js": { "node_modules/core-js": {
"version": "3.12.1", "version": "3.29.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.12.1.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz",
"integrity": "sha512-Ne9DKPHTObRuB09Dru5AjwKjY4cJHVGu+y5f7coGn1E9Grkc3p2iBwE9AI/nJzsE29mQF7oq+mhYYRqOMFN1Bw==" "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
}, },
"node_modules/core-js-compat": { "node_modules/core-js-compat": {
"version": "3.27.1", "version": "3.27.1",
@@ -4932,9 +4971,9 @@
} }
}, },
"node_modules/cross-spawn/node_modules/semver": { "node_modules/cross-spawn/node_modules/semver": {
"version": "5.7.1", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true, "dev": true,
"bin": { "bin": {
"semver": "bin/semver" "semver": "bin/semver"
@@ -5799,9 +5838,9 @@
} }
}, },
"node_modules/editorconfig/node_modules/semver": { "node_modules/editorconfig/node_modules/semver": {
"version": "5.7.1", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true, "dev": true,
"bin": { "bin": {
"semver": "bin/semver" "semver": "bin/semver"
@@ -6329,6 +6368,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/flatbuffers": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz",
"integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ=="
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.2", "version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
@@ -6476,9 +6520,9 @@
} }
}, },
"node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -6705,6 +6749,11 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"dev": true "dev": true
}, },
"node_modules/guid-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="
},
"node_modules/gzip-size": { "node_modules/gzip-size": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@@ -7821,6 +7870,11 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"node_modules/lower-case": { "node_modules/lower-case": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -8183,9 +8237,9 @@
} }
}, },
"node_modules/normalize-package-data/node_modules/semver": { "node_modules/normalize-package-data/node_modules/semver": {
"version": "5.7.1", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true, "dev": true,
"bin": { "bin": {
"semver": "bin/semver" "semver": "bin/semver"
@@ -8343,6 +8397,32 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/onnx-proto": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz",
"integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==",
"dependencies": {
"protobufjs": "^6.8.8"
}
},
"node_modules/onnxruntime-common": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.15.1.tgz",
"integrity": "sha512-Y89eJ8QmaRsPZPWLaX7mfqhj63ny47rSkQe80hIo+lvBQdrdXYR9VO362xvZulk9DFkCnXmGidprvgJ07bKsIQ=="
},
"node_modules/onnxruntime-web": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.15.1.tgz",
"integrity": "sha512-Ky4AXFLFyiGRu5KQJdDcbhdNcO0f2ND/8IPmTEwcKKIHpCwH6/Q9UoMpcoFz78lxGvnmmy+FFgA/Bs1HjdM6LA==",
"dependencies": {
"flatbuffers": "^1.12.0",
"guid-typescript": "^1.0.9",
"long": "^4.0.0",
"onnx-proto": "^4.0.4",
"onnxruntime-common": "~1.15.1",
"platform": "^1.3.6"
}
},
"node_modules/open": { "node_modules/open": {
"version": "8.4.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz",
@@ -8724,6 +8804,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"node_modules/popper.js": { "node_modules/popper.js": {
"version": "1.16.1", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
@@ -9345,6 +9430,31 @@
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=",
"dev": true "dev": true
}, },
"node_modules/protobufjs": {
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
"integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
"hasInstallScript": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^4.0.0"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -9807,9 +9917,9 @@
} }
}, },
"node_modules/sass-loader/node_modules/semver": { "node_modules/sass-loader/node_modules/semver": {
"version": "7.3.5", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -9860,9 +9970,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.0", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true, "dev": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -10623,9 +10733,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.1.6", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -10793,10 +10903,13 @@
"integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==" "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w=="
}, },
"node_modules/vue-cli-plugin-bootstrap-vue": { "node_modules/vue-cli-plugin-bootstrap-vue": {
"version": "0.7.0", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-bootstrap-vue/-/vue-cli-plugin-bootstrap-vue-0.7.0.tgz", "resolved": "https://registry.npmjs.org/vue-cli-plugin-bootstrap-vue/-/vue-cli-plugin-bootstrap-vue-0.8.2.tgz",
"integrity": "sha512-KYP7CpwbM6Xw94LR2G6y8ZqqTDKlYTgLDwsxaNUj8NyuAo/mpo2y6Q67T9CXjiPgJYVbWH5IzkhE0gHc+9xZKA==", "integrity": "sha512-PnWIioKYvfV45qsaZarBdrOSZCbKp8sisGp6KfBvTfSl2htHh7Ig7A40Lqqk3WadsSheZWF2fVvo9Dyz29G3vw==",
"dev": true "dev": true,
"dependencies": {
"lodash": "^4.17.21"
}
}, },
"node_modules/vue-color": { "node_modules/vue-color": {
"version": "2.8.1", "version": "2.8.1",
@@ -11039,9 +11152,9 @@
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.75.0", "version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.3", "@types/eslint-scope": "^3.7.3",
@@ -11617,6 +11730,15 @@
} }
} }
}, },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -11631,6 +11753,33 @@
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
} }
},
"node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true,
"engines": {
"node": ">=10"
}
} }
}, },
"dependencies": { "dependencies": {
@@ -12991,6 +13140,60 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true "dev": true
}, },
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"requires": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@sideway/address": { "@sideway/address": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
@@ -13203,6 +13406,11 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true "dev": true
}, },
"@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
},
"@types/mime": { "@types/mime": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
@@ -13218,8 +13426,7 @@
"@types/node": { "@types/node": {
"version": "18.11.18", "version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
"dev": true
}, },
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.1", "version": "2.4.1",
@@ -13279,6 +13486,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/underscore": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.6.tgz",
"integrity": "sha512-G2oC64I/sR817KDL2b2Mc7+diXyxcibyUeLMyexU4K/sG8hyt/YMlbBK0TVhx/YQ1ehfzgXhLuq2YQHIL4bXUQ==",
"dev": true
},
"@types/webpack-env": { "@types/webpack-env": {
"version": "1.16.0", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.0.tgz", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.0.tgz",
@@ -13379,9 +13592,9 @@
} }
}, },
"semver": { "semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -13624,9 +13837,9 @@
} }
}, },
"semver": { "semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -13957,9 +14170,9 @@
} }
}, },
"semver": { "semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -14064,9 +14277,9 @@
} }
}, },
"semver": { "semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -14818,9 +15031,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001444", "version": "1.0.30001515",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001444.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz",
"integrity": "sha512-ecER9xgJQVMqcrxThKptsW0pPxSae8R2RB87LNa+ivW9ppNWRHEplXcDzkCOP4LYWGj8hunXLqaiC41iBATNyg==", "integrity": "sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==",
"dev": true "dev": true
}, },
"case-sensitive-paths-webpack-plugin": { "case-sensitive-paths-webpack-plugin": {
@@ -14996,33 +15209,6 @@
"requires": { "requires": {
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
} }
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
}
},
"yargs-parser": {
"version": "20.2.7",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz",
"integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==",
"dev": true
} }
} }
}, },
@@ -15262,9 +15448,9 @@
"dev": true "dev": true
}, },
"core-js": { "core-js": {
"version": "3.12.1", "version": "3.29.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.12.1.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz",
"integrity": "sha512-Ne9DKPHTObRuB09Dru5AjwKjY4cJHVGu+y5f7coGn1E9Grkc3p2iBwE9AI/nJzsE29mQF7oq+mhYYRqOMFN1Bw==" "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw=="
}, },
"core-js-compat": { "core-js-compat": {
"version": "3.27.1", "version": "3.27.1",
@@ -15308,9 +15494,9 @@
}, },
"dependencies": { "dependencies": {
"semver": { "semver": {
"version": "5.7.1", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true "dev": true
} }
} }
@@ -16005,9 +16191,9 @@
} }
}, },
"semver": { "semver": {
"version": "5.7.1", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true "dev": true
}, },
"yallist": { "yallist": {
@@ -16440,6 +16626,11 @@
"path-exists": "^4.0.0" "path-exists": "^4.0.0"
} }
}, },
"flatbuffers": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz",
"integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ=="
},
"follow-redirects": { "follow-redirects": {
"version": "1.15.2", "version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
@@ -16527,9 +16718,9 @@
} }
}, },
"semver": { "semver": {
"version": "7.3.8", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -16699,6 +16890,11 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"dev": true "dev": true
}, },
"guid-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="
},
"gzip-size": { "gzip-size": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@@ -17553,6 +17749,11 @@
} }
} }
}, },
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"lower-case": { "lower-case": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -17842,9 +18043,9 @@
}, },
"dependencies": { "dependencies": {
"semver": { "semver": {
"version": "5.7.1", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true "dev": true
} }
} }
@@ -17959,6 +18160,32 @@
"mimic-fn": "^1.0.0" "mimic-fn": "^1.0.0"
} }
}, },
"onnx-proto": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz",
"integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==",
"requires": {
"protobufjs": "^6.8.8"
}
},
"onnxruntime-common": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.15.1.tgz",
"integrity": "sha512-Y89eJ8QmaRsPZPWLaX7mfqhj63ny47rSkQe80hIo+lvBQdrdXYR9VO362xvZulk9DFkCnXmGidprvgJ07bKsIQ=="
},
"onnxruntime-web": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.15.1.tgz",
"integrity": "sha512-Ky4AXFLFyiGRu5KQJdDcbhdNcO0f2ND/8IPmTEwcKKIHpCwH6/Q9UoMpcoFz78lxGvnmmy+FFgA/Bs1HjdM6LA==",
"requires": {
"flatbuffers": "^1.12.0",
"guid-typescript": "^1.0.9",
"long": "^4.0.0",
"onnx-proto": "^4.0.4",
"onnxruntime-common": "~1.15.1",
"platform": "^1.3.6"
}
},
"open": { "open": {
"version": "8.4.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz",
@@ -18245,6 +18472,11 @@
"find-up": "^4.0.0" "find-up": "^4.0.0"
} }
}, },
"platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"popper.js": { "popper.js": {
"version": "1.16.1", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
@@ -18653,6 +18885,26 @@
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=",
"dev": true "dev": true
}, },
"protobufjs": {
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
"integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
"requires": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^4.0.0"
}
},
"proxy-addr": { "proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -19010,9 +19262,9 @@
} }
}, },
"semver": { "semver": {
"version": "7.3.5", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -19053,9 +19305,9 @@
} }
}, },
"semver": { "semver": {
"version": "6.3.0", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true "dev": true
}, },
"send": { "send": {
@@ -19661,9 +19913,9 @@
} }
}, },
"typescript": { "typescript": {
"version": "4.1.6", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true "dev": true
}, },
"underscore": { "underscore": {
@@ -19781,10 +20033,13 @@
"integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==" "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w=="
}, },
"vue-cli-plugin-bootstrap-vue": { "vue-cli-plugin-bootstrap-vue": {
"version": "0.7.0", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-bootstrap-vue/-/vue-cli-plugin-bootstrap-vue-0.7.0.tgz", "resolved": "https://registry.npmjs.org/vue-cli-plugin-bootstrap-vue/-/vue-cli-plugin-bootstrap-vue-0.8.2.tgz",
"integrity": "sha512-KYP7CpwbM6Xw94LR2G6y8ZqqTDKlYTgLDwsxaNUj8NyuAo/mpo2y6Q67T9CXjiPgJYVbWH5IzkhE0gHc+9xZKA==", "integrity": "sha512-PnWIioKYvfV45qsaZarBdrOSZCbKp8sisGp6KfBvTfSl2htHh7Ig7A40Lqqk3WadsSheZWF2fVvo9Dyz29G3vw==",
"dev": true "dev": true,
"requires": {
"lodash": "^4.17.21"
}
}, },
"vue-color": { "vue-color": {
"version": "2.8.1", "version": "2.8.1",
@@ -19990,9 +20245,9 @@
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
}, },
"webpack": { "webpack": {
"version": "5.75.0", "version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/eslint-scope": "^3.7.3", "@types/eslint-scope": "^3.7.3",
@@ -20399,6 +20654,12 @@
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true
},
"yallist": { "yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -20410,6 +20671,27 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true "dev": true
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
}
},
"yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "sist2", "name": "sist2",
"version": "2.11.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@@ -17,6 +17,7 @@
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"fslightbox-vue": "fslightbox-vue.tgz", "fslightbox-vue": "fslightbox-vue.tgz",
"nouislider": "^15.2.0", "nouislider": "^15.2.0",
"onnxruntime-web": "^1.15.1",
"underscore": "^1.13.1", "underscore": "^1.13.1",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-color": "^2.8.1", "vue-color": "^2.8.1",
@@ -28,6 +29,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@types/underscore": "^1.11.6",
"@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8", "@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-typescript": "^5.0.8", "@vue/cli-plugin-typescript": "^5.0.8",
@@ -42,8 +44,8 @@
"portal-vue": "^2.1.7", "portal-vue": "^2.1.7",
"sass": "^1.26.11", "sass": "^1.26.11",
"sass-loader": "^10.0.2", "sass-loader": "^10.0.2",
"typescript": "~4.1.5", "typescript": "^4.9.5",
"vue-cli-plugin-bootstrap-vue": "~0.7.0", "vue-cli-plugin-bootstrap-vue": "~0.8.2",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
}, },
"browserslist": [ "browserslist": [

View File

@@ -19,12 +19,6 @@
We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue. Please enable it to continue.
</strong> </strong>
<br/>
<strong>
Nous sommes désolés mais <%= htmlWebpackPlugin.options.title %> ne fonctionne pas correctement
si JavaScript est activé.
Veuillez l'activer pour continuer.
</strong>
</div> </div>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>

View File

@@ -1,383 +1,410 @@
<template> <template>
<div id="app" :class="getClass()" v-if="!authLoading"> <div id="app" :class="getClass()" v-if="!authLoading">
<NavBar></NavBar> <NavBar></NavBar>
<router-view v-if="!configLoading"/> <router-view v-if="!configLoading"/>
</div>
<div class="loading-page" v-else>
<div class="loading-spinners">
<b-spinner type="grow" variant="primary"></b-spinner>
<b-spinner type="grow" variant="primary"></b-spinner>
<b-spinner type="grow" variant="primary"></b-spinner>
</div> </div>
<div class="loading-text"> <div class="loading-page" v-else>
Loading Chargement 装载 Wird geladen <div class="loading-spinners">
<b-spinner type="grow" variant="primary"></b-spinner>
<b-spinner type="grow" variant="primary"></b-spinner>
<b-spinner type="grow" variant="primary"></b-spinner>
</div>
<div class="loading-text">
Loading Chargement 装载 Wird geladen Ładowanie
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
import NavBar from "@/components/NavBar"; import NavBar from "@/components/NavBar";
import {mapActions, mapGetters, mapMutations} from "vuex"; import {mapActions, mapGetters, mapMutations} from "vuex";
import Sist2Api from "@/Sist2Api"; import Sist2Api from "@/Sist2Api";
import ModelsRepo from "@/ml/modelsRepo";
import {setupAuth0} from "@/main"; import {setupAuth0} from "@/main";
import Sist2ElasticsearchQuery from "@/Sist2ElasticsearchQuery";
import Sist2SqliteQuery from "@/Sist2SqliteQuery";
export default { export default {
components: {NavBar}, components: {NavBar},
data() { data() {
return { return {
configLoading: false, configLoading: false,
authLoading: true, authLoading: true,
sist2InfoLoading: true sist2InfoLoading: true
} }
}, },
computed: { computed: {
...mapGetters(["optTheme"]), ...mapGetters(["optTheme"]),
}, },
mounted() { mounted() {
this.$store.dispatch("loadConfiguration").then(() => { this.$store.dispatch("loadConfiguration").then(() => {
this.$root.$i18n.locale = this.$store.state.optLang; this.$root.$i18n.locale = this.$store.state.optLang;
}); ModelsRepo.init(this.$store.getters.mlRepositoryList).catch(err => {
this.$bvToast.toast(
this.$t("ml.repoFetchError"),
{
title: this.$t("ml.repoFetchErrorTitle"),
noAutoHide: true,
toaster: "b-toaster-bottom-right",
headerClass: "toast-header-warning",
bodyClass: "toast-body-warning",
});
});
});
this.$store.subscribe((mutation) => { this.$store.subscribe((mutation) => {
if (mutation.type === "setOptLang") { if (mutation.type === "setOptLang") {
this.$root.$i18n.locale = mutation.payload; this.$root.$i18n.locale = mutation.payload;
this.configLoading = true; this.configLoading = true;
window.setTimeout(() => this.configLoading = false, 10); window.setTimeout(() => this.configLoading = false, 10);
}
if (mutation.type === "setAuth0Token") {
this.authLoading = false;
}
});
Sist2Api.getSist2Info().then(data => {
if (data.auth0Enabled) {
this.authLoading = true;
setupAuth0(data.auth0Domain, data.auth0ClientId, data.auth0Audience)
this.$auth.$watch("loading", loading => {
if (loading === false) {
if (!this.$auth.isAuthenticated) {
this.$auth.loginWithRedirect();
return;
} }
// Remove "code" param if (mutation.type === "setAuth0Token") {
window.history.replaceState({}, "", "/" + window.location.hash); this.authLoading = false;
}
this.$store.dispatch("loadAuth0Token");
}
}); });
} else {
this.authLoading = false;
}
this.setSist2Info(data); Sist2Api.getSist2Info().then(data => {
this.setIndices(data.indices)
}); if (data.auth0Enabled) {
}, this.authLoading = true;
methods: { setupAuth0(data.auth0Domain, data.auth0ClientId, data.auth0Audience)
...mapActions(["setSist2Info",]),
...mapMutations(["setIndices",]), this.$auth.$watch("loading", loading => {
getClass() { if (loading === false) {
return {
"theme-light": this.optTheme === "light", if (!this.$auth.isAuthenticated) {
"theme-black": this.optTheme === "black", this.$auth.loginWithRedirect();
} return;
}
// Remove "code" param
window.history.replaceState({}, "", "/" + window.location.hash);
this.$store.dispatch("loadAuth0Token");
}
});
} else {
this.authLoading = false;
}
this.setSist2Info(data);
this.setIndices(data.indices)
if (Sist2Api.backend() === "sqlite") {
Sist2Api.init(Sist2SqliteQuery.searchQuery);
this.$store.commit("setUiSqliteMode", true);
} else {
Sist2Api.init(Sist2ElasticsearchQuery.searchQuery);
}
});
},
methods: {
...mapActions(["setSist2Info",]),
...mapMutations(["setIndices",]),
getClass() {
return {
"theme-light": this.optTheme === "light",
"theme-black": this.optTheme === "black",
}
}
} }
} ,
,
} }
</script> </script>
<style> <style>
html, body { html, body {
height: 100%; height: 100%;
} }
#app { #app {
/*font-family: Avenir, Helvetica, Arial, sans-serif;*/ /*font-family: Avenir, Helvetica, Arial, sans-serif;*/
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
/*text-align: center;*/ /*text-align: center;*/
color: #2c3e50; color: #2c3e50;
padding-bottom: 1em; padding-bottom: 1em;
min-height: 100%; min-height: 100%;
} }
/*Black theme*/ /*Black theme*/
.theme-black { .theme-black {
background-color: #000; background-color: #000;
} }
.theme-black .card, .theme-black .modal-content { .theme-black .card, .theme-black .modal-content {
background: #212121; background: #212121;
color: #e0e0e0; color: #e0e0e0;
border-radius: 1px; border-radius: 1px;
border: none; border: none;
} }
.theme-black .table { .theme-black .table {
color: #e0e0e0; color: #e0e0e0;
} }
.theme-black .table td, .theme-black .table th { .theme-black .table td, .theme-black .table th {
border: none; border: none;
} }
.theme-black .table thead th { .theme-black .table thead th {
border-bottom: 1px solid #646464; border-bottom: 1px solid #646464;
} }
.theme-black .custom-select { .theme-black .custom-select {
overflow: auto; overflow: auto;
background-color: #37474F; background-color: #37474F;
border: 1px solid #616161; border: 1px solid #616161;
color: #bdbdbd; color: #bdbdbd;
} }
.theme-black .custom-select:focus { .theme-black .custom-select:focus {
border-color: #757575; border-color: #757575;
outline: 0; outline: 0;
box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25); box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
} }
.theme-black .inspire-tree .selected > .wholerow, .theme-black .inspire-tree .selected > .title-wrap:hover + .wholerow { .theme-black .inspire-tree .selected > .wholerow, .theme-black .inspire-tree .selected > .title-wrap:hover + .wholerow {
background: none !important; background: none !important;
} }
.theme-black .inspire-tree .icon-expand::before, .theme-black .inspire-tree .icon-collapse::before { .theme-black .inspire-tree .icon-expand::before, .theme-black .inspire-tree .icon-collapse::before {
background-color: black !important; background-color: black !important;
} }
.theme-black .inspire-tree .title { .theme-black .inspire-tree .title {
color: #eee; color: #eee;
} }
.theme-black .inspire-tree { .theme-black .inspire-tree {
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
font-family: Helvetica, Nueue, Verdana, sans-serif; font-family: Helvetica, Nueue, Verdana, sans-serif;
max-height: 350px; max-height: 350px;
overflow: auto; overflow: auto;
} }
.inspire-tree [type=checkbox] { .inspire-tree [type=checkbox] {
left: 22px !important; left: 22px !important;
top: 7px !important; top: 7px !important;
} }
.theme-black .form-control { .theme-black .form-control {
background-color: #37474F; background-color: #37474F;
border: 1px solid #616161; border: 1px solid #616161;
color: #dbdbdb !important; color: #dbdbdb !important;
} }
.theme-black .form-control:focus { .theme-black .form-control:focus {
background-color: #546E7A; background-color: #546E7A;
color: #fff; color: #fff;
} }
.theme-black .input-group-text, .theme-black .default-input { .theme-black .input-group-text, .theme-black .default-input {
background: #37474F !important; background: #37474F !important;
border: 1px solid #616161 !important; border: 1px solid #616161 !important;
color: #dbdbdb !important; color: #dbdbdb !important;
} }
.theme-black ::placeholder { .theme-black ::placeholder {
color: #BDBDBD !important; color: #BDBDBD !important;
opacity: 1; opacity: 1;
} }
.theme-black .nav-tabs .nav-link { .theme-black .nav-tabs .nav-link {
color: #e0e0e0; color: #e0e0e0;
border-radius: 0; border-radius: 0;
} }
.theme-black .nav-tabs .nav-item.show .nav-link, .theme-black .nav-tabs .nav-link.active { .theme-black .nav-tabs .nav-item.show .nav-link, .theme-black .nav-tabs .nav-link.active {
background-color: #212121; background-color: #212121;
border-color: #616161 #616161 #212121; border-color: #616161 #616161 #212121;
color: #e0e0e0; color: #e0e0e0;
} }
.theme-black .nav-tabs .nav-link:focus, .theme-black .nav-tabs .nav-link:focus { .theme-black .nav-tabs .nav-link:focus, .theme-black .nav-tabs .nav-link:focus {
border-color: #616161 #616161 #212121; border-color: #616161 #616161 #212121;
color: #e0e0e0; color: #e0e0e0;
} }
.theme-black .nav-tabs .nav-link:focus, .theme-black .nav-tabs .nav-link:hover { .theme-black .nav-tabs .nav-link:focus, .theme-black .nav-tabs .nav-link:hover {
border-color: #e0e0e0 #e0e0e0 #212121; border-color: #e0e0e0 #e0e0e0 #212121;
color: #e0e0e0; color: #e0e0e0;
} }
.theme-black .nav-tabs { .theme-black .nav-tabs {
border-bottom: #616161; border-bottom: #616161;
} }
.theme-black a:hover, .theme-black .btn:hover { .theme-black a:hover, .theme-black .btn:hover {
color: #fff; color: #fff;
} }
.theme-black .b-dropdown a:hover { .theme-black .b-dropdown a:hover {
color: inherit; color: inherit;
} }
.theme-black .btn { .theme-black .btn {
color: #eee; color: #eee;
} }
.theme-black .modal-header .close { .theme-black .modal-header .close {
color: #e0e0e0; color: #e0e0e0;
text-shadow: none; text-shadow: none;
} }
.theme-black .modal-header { .theme-black .modal-header {
border-bottom: 1px solid #646464; border-bottom: 1px solid #646464;
} }
/* -------------------------- */ /* -------------------------- */
#nav { #nav {
padding: 30px; padding: 30px;
} }
#nav a { #nav a {
font-weight: bold; font-weight: bold;
color: #2c3e50; color: #2c3e50;
} }
#nav a.router-link-exact-active { #nav a.router-link-exact-active {
color: #42b983; color: #42b983;
} }
.mobile { .mobile {
display: none; display: none;
} }
.container { .container {
padding-top: 1em; padding-top: 1em;
} }
@media (max-width: 650px) { @media (max-width: 650px) {
.mobile { .mobile {
display: initial; display: initial;
} }
.not-mobile { .not-mobile {
display: none; display: none;
} }
.grid-single-column .fit { .grid-single-column .fit {
max-height: none !important; max-height: none !important;
} }
.container { .container {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
padding-top: 0 padding-top: 0
} }
.lightbox-caption { .lightbox-caption {
display: none; display: none;
} }
} }
.info-icon { .info-icon {
width: 1rem; width: 1rem;
margin-right: 0.2rem; min-width: 1rem;
cursor: pointer; margin-right: 0.2rem;
line-height: 1rem; cursor: pointer;
height: 1rem; line-height: 1rem;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==); height: 1rem;
filter: brightness(45%); min-height: 1rem;
display: block; background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==);
filter: brightness(45%);
display: block;
}
.theme-black .info-icon {
filter: brightness(80%);
} }
.tabs { .tabs {
margin-top: 10px; margin-top: 10px;
} }
.modal-title { .modal-title {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
@media screen and (min-width: 1500px) { @media screen and (min-width: 1500px) {
.container { .container {
max-width: 1440px; max-width: 1440px;
} }
} }
.noUi-connects { .noUi-connects {
border-radius: 1px !important; border-radius: 1px !important;
} }
mark { mark {
background: #fff217; background: #fff217;
border-radius: 0; border-radius: 0;
padding: 1px 0; padding: 1px 0;
color: inherit; color: inherit;
} }
.theme-black mark { .theme-black mark {
background: rgba(251, 191, 41, 0.25); background: rgba(251, 191, 41, 0.25);
border-radius: 0; border-radius: 0;
padding: 1px 0; padding: 1px 0;
color: inherit; color: inherit;
} }
.theme-black .content-div mark { .theme-black .content-div mark {
background: rgba(251, 191, 41, 0.40); background: rgba(251, 191, 41, 0.40);
color: white; color: white;
} }
.content-div { .content-div {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 13px; font-size: 13px;
padding: 1em; padding: 1em;
background-color: #f5f5f5; background-color: #f5f5f5;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
margin: 3px; margin: 3px;
white-space: normal; white-space: normal;
color: #000; color: #000;
overflow: hidden; overflow: hidden;
} }
.theme-black .content-div { .theme-black .content-div {
background-color: #37474F; background-color: #37474F;
border: 1px solid #616161; border: 1px solid #616161;
color: #E0E0E0FF; color: #E0E0E0FF;
} }
.graph { .graph {
display: inline-block; display: inline-block;
width: 40%; width: 40%;
} }
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
.loading-page { .loading-page {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
gap: 15px gap: 15px
} }
.loading-spinners { .loading-spinners {
display: flex; display: flex;
gap: 10px; gap: 10px;
} }
.loading-text { .loading-text {
text-align: center; text-align: center;
} }
</style> </style>

View File

@@ -1,5 +1,7 @@
import axios from "axios"; import axios from "axios";
import {ext, strUnescape, lum} from "./util"; import {ext, strUnescape, lum} from "./util";
import Sist2Query from "@/Sist2ElasticsearchQuery";
import store from "@/store";
export interface EsTag { export interface EsTag {
id: string id: string
@@ -23,6 +25,7 @@ export interface Index {
id: string id: string
idPrefix: string idPrefix: string
timestamp: number timestamp: number
models: []
} }
export interface EsHit { export interface EsHit {
@@ -99,12 +102,31 @@ export interface EsResult {
class Sist2Api { class Sist2Api {
private baseUrl: string private readonly baseUrl: string
private sist2Info: any
private queryfunc: () => EsResult;
constructor(baseUrl: string) { constructor(baseUrl: string) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
} }
init(queryFunc: () => EsResult) {
this.queryfunc = queryFunc;
}
backend() {
return this.sist2Info.searchBackend;
}
models() {
const allModels = this.sist2Info.indices
.map(idx => idx.models)
.flat();
return allModels
.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i)
}
getSist2Info(): Promise<any> { getSist2Info(): Promise<any> {
return axios.get(`${this.baseUrl}i`).then(resp => { return axios.get(`${this.baseUrl}i`).then(resp => {
const indices = resp.data.indices as Index[]; const indices = resp.data.indices as Index[];
@@ -115,10 +137,13 @@ class Sist2Api {
name: idx.name, name: idx.name,
timestamp: idx.timestamp, timestamp: idx.timestamp,
version: idx.version, version: idx.version,
idPrefix: getIdPrefix(indices, idx.id) models: idx.models,
idPrefix: getIdPrefix(indices, idx.id),
} as Index; } as Index;
}); });
this.sist2Info = resp.data;
return resp.data; return resp.data;
}) })
} }
@@ -219,6 +244,14 @@ class Sist2Api {
} as Tag; } as Tag;
} }
search(): Promise<EsResult> {
if (this.backend() == "sqlite") {
return this.ftsQuery(this.queryfunc())
} else {
return this.esQuery(this.queryfunc());
}
}
esQuery(query: any): Promise<EsResult> { esQuery(query: any): Promise<EsResult> {
return axios.post(`${this.baseUrl}es`, query).then(resp => { return axios.post(`${this.baseUrl}es`, query).then(resp => {
const res = resp.data as EsResult; const res = resp.data as EsResult;
@@ -237,7 +270,30 @@ class Sist2Api {
}); });
} }
getMimeTypes(query = undefined) { ftsQuery(query: any): Promise<EsResult> {
return axios.post(`${this.baseUrl}fts/search`, query).then(resp => {
const res = resp.data as any;
if (res.hits.hits) {
res.hits.hits.forEach(hit => {
hit["_source"]["name"] = strUnescape(hit["_source"]["name"]);
hit["_source"]["path"] = strUnescape(hit["_source"]["path"]);
this.setHitProps(hit);
this.setHitTags(hit);
if ("highlight" in hit) {
hit["highlight"]["name"] = [hit["highlight"]["name"]];
hit["highlight"]["content"] = [hit["highlight"]["content"]];
}
});
}
return res;
});
}
private getMimeTypesEs(query) {
const AGGS = { const AGGS = {
mimeTypes: { mimeTypes: {
terms: { terms: {
@@ -258,48 +314,70 @@ class Sist2Api {
} }
return this.esQuery(query).then(resp => { return this.esQuery(query).then(resp => {
const mimeMap: any[] = []; return resp["aggregations"]["mimeTypes"]["buckets"].map(bucket => ({
const buckets = resp["aggregations"]["mimeTypes"]["buckets"]; mime: bucket.key,
count: bucket.doc_count
}));
buckets.sort((a: any, b: any) => a.key > b.key).forEach((bucket: any) => { });
const tmp = bucket["key"].split("/"); }
const category = tmp[0];
const mime = tmp[1];
let category_exists = false; private getMimeTypesSqlite(): Promise<[{ mime: string, count: number }]> {
return axios.get(`${this.baseUrl}fts/mimetypes`)
.then(resp => {
return resp.data;
});
}
const child = { async getMimeTypes(query = undefined) {
"id": bucket["key"], let buckets;
"text": `${mime} (${bucket["doc_count"]})`
};
mimeMap.forEach(node => { if (this.backend() == "sqlite") {
if (node.text === category) { buckets = await this.getMimeTypesSqlite();
node.children.push(child); } else {
category_exists = true; buckets = await this.getMimeTypesEs(query);
} }
});
if (!category_exists) { const mimeMap: any[] = [];
mimeMap.push({text: category, children: [child], id: category});
} buckets.sort((a: any, b: any) => a.mime > b.mime).forEach((bucket: any) => {
}) const tmp = bucket.mime.split("/");
const category = tmp[0];
const mime = tmp[1];
let category_exists = false;
const child = {
"id": bucket.mime,
"text": `${mime} (${bucket.count})`
};
mimeMap.forEach(node => { mimeMap.forEach(node => {
if (node.children) { if (node.text === category) {
node.children.sort((a, b) => a.id.localeCompare(b.id)); node.children.push(child);
category_exists = true;
} }
}) });
mimeMap.sort((a, b) => a.id.localeCompare(b.id))
return {buckets, mimeMap}; if (!category_exists) {
}); mimeMap.push({text: category, children: [child], id: category});
}
})
mimeMap.forEach(node => {
if (node.children) {
node.children.sort((a, b) => a.id.localeCompare(b.id));
}
})
mimeMap.sort((a, b) => a.id.localeCompare(b.id))
return {buckets, mimeMap};
} }
_createEsTag(tag: string, count: number): EsTag { _createEsTag(tag: string, count: number): EsTag {
const tokens = tag.split("."); const tokens = tag.split(".");
if (/.*\.#[0-9a-f]{6}/.test(tag)) { if (/.*\.#[0-9a-fA-F]{6}/.test(tag)) {
return { return {
id: tokens.slice(0, -1).join("."), id: tokens.slice(0, -1).join("."),
color: tokens.pop(), color: tokens.pop(),
@@ -316,32 +394,48 @@ class Sist2Api {
}; };
} }
getTags() { private getTagsEs() {
return this.esQuery({ return this.esQuery({
aggs: { aggs: {
tags: { tags: {
terms: { terms: {
field: "tag", field: "tag",
size: 10000 size: 65535
} }
} }
}, },
size: 0, size: 0,
}).then(resp => { }).then(resp => {
const seen = new Set(); return resp["aggregations"]["tags"]["buckets"]
const tags = resp["aggregations"]["tags"]["buckets"]
.sort((a: any, b: any) => a["key"].localeCompare(b["key"])) .sort((a: any, b: any) => a["key"].localeCompare(b["key"]))
.map((bucket: any) => this._createEsTag(bucket["key"], bucket["doc_count"])); .map((bucket: any) => this._createEsTag(bucket["key"], bucket["doc_count"]));
});
}
// Remove duplicates (same tag with different color) private getTagsSqlite() {
return tags.filter((t: EsTag) => { return axios.get(`${this.baseUrl}/fts/tags`)
if (seen.has(t.id)) { .then(resp => {
return false; return resp.data.map(tag => this._createEsTag(tag.tag, tag.count))
}
seen.add(t.id);
return true;
}); });
}
async getTags(): Promise<EsTag[]> {
let tags;
if (this.backend() == "sqlite") {
tags = await this.getTagsSqlite();
} else {
tags = await this.getTagsEs();
}
// Remove duplicates (same tag with different color)
const seen = new Set();
return tags.filter((t: EsTag) => {
if (seen.has(t.id)) {
return false;
}
seen.add(t.id);
return true;
}); });
} }
@@ -361,20 +455,277 @@ class Sist2Api {
}); });
} }
getTreemapCsvUrl(indexId: string) { searchPaths(indexId, minDepth, maxDepth, prefix = null) {
return `${this.baseUrl}s/${indexId}/1`; if (this.backend() == "sqlite") {
return this.searchPathsSqlite(indexId, minDepth, minDepth, prefix);
} else {
return this.searchPathsEs(indexId, minDepth, maxDepth, prefix);
}
} }
getMimeCsvUrl(indexId: string) { private searchPathsSqlite(indexId, minDepth, maxDepth, prefix) {
return `${this.baseUrl}s/${indexId}/2`; return axios.post(`${this.baseUrl}fts/paths`, {
indexId, minDepth, maxDepth, prefix
}).then(resp => {
return resp.data;
});
} }
getSizeCsv(indexId: string) { private searchPathsEs(indexId, minDepth, maxDepth, prefix): Promise<[{ path: string, count: number }]> {
return `${this.baseUrl}s/${indexId}/3`;
const query = {
query: {
bool: {
filter: [
{term: {index: indexId}},
{range: {_depth: {gte: minDepth, lte: maxDepth}}},
]
}
},
aggs: {
paths: {
terms: {
field: "path",
size: 10000
}
}
},
size: 0
};
if (prefix != null) {
query["query"]["bool"]["must"] = {
prefix: {
path: prefix,
}
};
}
return this.esQuery(query).then(resp => {
const buckets = resp["aggregations"]["paths"]["buckets"];
if (!buckets) {
return [];
}
return buckets
.map(bucket => ({
path: bucket.key,
count: bucket.doc_count
}));
});
} }
getDateCsv(indexId: string) { private getDateRangeSqlite() {
return `${this.baseUrl}s/${indexId}/4`; return axios.get(`${this.baseUrl}fts/dateRange`)
.then(resp => ({
min: resp.data.dateMin,
max: resp.data.dateMax,
}));
}
getDateRange(): Promise<{ min: number, max: number }> {
if (this.backend() == "sqlite") {
return this.getDateRangeSqlite();
} else {
return this.getDateRangeEs();
}
}
private getDateRangeEs() {
return this.esQuery({
// TODO: filter current selected indices
aggs: {
dateMin: {min: {field: "mtime"}},
dateMax: {max: {field: "mtime"}},
},
size: 0
}).then(res => {
const range = {
min: res.aggregations.dateMin.value / 1000,
max: res.aggregations.dateMax.value / 1000,
}
if (range.min == null) {
range.min = 0;
range.max = 1;
} else if (range.min == range.max) {
range.max += 1;
}
return range;
});
}
private getPathSuggestionsSqlite(text: string) {
return axios.post(`${this.baseUrl}fts/paths`, {
prefix: text,
minDepth: 1,
maxDepth: 10000
}).then(resp => {
return resp.data.map(bucket => bucket.path);
})
}
private getPathSuggestionsEs(text) {
return this.esQuery({
suggest: {
path: {
prefix: text,
completion: {
field: "suggest-path",
skip_duplicates: true,
size: 10000
}
}
}
}).then(resp => {
return resp["suggest"]["path"][0]["options"]
.map(opt => opt["_source"]["path"]);
});
}
getPathSuggestions(text: string): Promise<string[]> {
if (this.backend() == "sqlite") {
return this.getPathSuggestionsSqlite(text);
} else {
return this.getPathSuggestionsEs(text)
}
}
getTreemapStat(indexId: string) {
return `${this.baseUrl}s/${indexId}/TMAP`;
}
getMimeStat(indexId: string) {
return `${this.baseUrl}s/${indexId}/MAGG`;
}
getSizeStat(indexId: string) {
return `${this.baseUrl}s/${indexId}/SAGG`;
}
getDateStat(indexId: string) {
return `${this.baseUrl}s/${indexId}/DAGG`;
}
private getDocumentEs(docId: string, highlight: boolean, fuzzy: boolean) {
const query = Sist2Query.searchQuery();
if (highlight) {
const fields = fuzzy
? {"content.nGram": {}}
: {content: {}};
query.highlight = {
pre_tags: ["<mark>"],
post_tags: ["</mark>"],
number_of_fragments: 0,
fields,
};
if (!store.state.sist2Info.esVersionLegacy) {
query.highlight.max_analyzed_offset = 999_999;
}
}
if ("knn" in query) {
query.query = {
bool: {
must: []
}
};
delete query.knn;
}
if ("function_score" in query.query) {
query.query = query.query.function_score.query;
}
if (!("must" in query.query.bool)) {
query.query.bool.must = [];
} else if (!Array.isArray(query.query.bool.must)) {
query.query.bool.must = [query.query.bool.must];
}
query.query.bool.must.push({match: {_id: docId}});
delete query["sort"];
delete query["aggs"];
delete query["search_after"];
delete query.query["function_score"];
query._source = {
includes: ["content", "name", "path", "extension"]
}
query.size = 1;
return this.esQuery(query).then(resp => {
if (resp.hits.hits.length === 1) {
return resp.hits.hits[0];
}
return null;
});
}
private getDocumentSqlite(docId: string): Promise<EsHit> {
return axios.get(`${this.baseUrl}/fts/d/${docId}`)
.then(resp => ({
_source: resp.data
} as EsHit));
}
getDocument(docId: string, highlight: boolean, fuzzy: boolean): Promise<EsHit | null> {
if (this.backend() == "sqlite") {
return this.getDocumentSqlite(docId);
} else {
return this.getDocumentEs(docId, highlight, fuzzy);
}
}
getTagSuggestions(prefix: string): Promise<string[]> {
if (this.backend() == "sqlite") {
return this.getTagSuggestionsSqlite(prefix);
} else {
return this.getTagSuggestionsEs(prefix);
}
}
private getTagSuggestionsSqlite(prefix): Promise<string[]> {
return axios.post(`${this.baseUrl}/fts/suggestTags`, prefix)
.then(resp => (resp.data));
}
private getTagSuggestionsEs(prefix): Promise<string[]> {
return this.esQuery({
suggest: {
tag: {
prefix: prefix,
completion: {
field: "suggest-tag",
skip_duplicates: true,
size: 10000
}
}
}
}).then(resp => {
const result = [];
resp["suggest"]["tag"][0]["options"].map(opt => opt["_source"]["tag"]).forEach(tags => {
tags.forEach(tag => {
const t = tag.slice(0, -8);
if (!result.find(x => x.slice(0, -8) === t)) {
result.push(tag);
}
});
});
return result;
});
}
getEmbeddings(indexId, docId, modelId) {
return axios.post(`${this.baseUrl}/e/${indexId}/${docId}/${modelId.toString().padStart(3, '0')}`)
.then(resp => (resp.data));
} }
} }

View File

@@ -1,5 +1,5 @@
import store from "./store"; import store from "./store";
import {EsHit, Index} from "@/Sist2Api"; import sist2Api, {EsHit, Index} from "@/Sist2Api";
const SORT_MODES = { const SORT_MODES = {
score: { score: {
@@ -60,14 +60,7 @@ const SORT_MODES = {
} }
} as any; } as any;
interface SortMode { class Sist2ElasticsearchQuery {
text: string
mode: any[]
key: (hit: EsHit) => any
}
class Sist2Query {
searchQuery(blankSearch: boolean = false): any { searchQuery(blankSearch: boolean = false): any {
@@ -86,8 +79,10 @@ class Sist2Query {
const selectedIndexIds = getters.selectedIndices.map((idx: Index) => idx.id) const selectedIndexIds = getters.selectedIndices.map((idx: Index) => idx.id)
const selectedMimeTypes = getters.selectedMimeTypes; const selectedMimeTypes = getters.selectedMimeTypes;
const selectedTags = getters.selectedTags; const selectedTags = getters.selectedTags;
const sortMode = getters.embedding ? "score" : getters.sortMode;
const legacyES = store.state.sist2Info.esVersionLegacy; const legacyES = store.state.sist2Info.esVersionLegacy;
const hasKnn = store.state.sist2Info.esVersionHasKnn;
const filters = [ const filters = [
{terms: {index: selectedIndexIds}} {terms: {index: selectedIndexIds}}
@@ -169,31 +164,76 @@ class Sist2Query {
const q = { const q = {
_source: { _source: {
excludes: ["content", "_tie"] excludes: ["content", "_tie", "emb.*"]
}, },
query: { query: {
bool: { bool: {
filter: filters, filter: filters,
} }
}, },
sort: SORT_MODES[getters.sortMode].mode, sort: SORT_MODES[sortMode].mode,
aggs:
{
total_size: {"sum": {"field": "size"}},
total_count: {"value_count": {"field": "size"}}
},
size: size, size: size,
} as any; } as any;
if (!after) {
q.aggs = {
total_size: {"sum": {"field": "size"}},
total_count: {"value_count": {"field": "size"}}
};
}
if (!empty && !blankSearch) { if (!empty && !blankSearch) {
q.query.bool.must = query; if (getters.embedding) {
filters.push(query)
} else {
q.query.bool.must = query;
}
}
if (getters.embedding) {
delete q.query;
const field = "emb." + sist2Api.models().find(m => m.id == getters.embeddingsModel).path;
if (hasKnn) {
// Use knn (8.8+)
q.knn = {
field: field,
query_vector: getters.embedding,
k: 600,
num_candidates: 600,
filter: filters
}
} else {
// Use brute-force as a fallback
filters.push({exists: {field: field}});
q.query = {
function_score: {
query: {
bool: {
must: filters,
}
},
script_score: {
script: {
source: `cosineSimilarity(params.query_vector, "${field}") + 1.0`,
params: {query_vector: getters.embedding}
}
}
}
}
}
} }
if (after) { if (after) {
q.search_after = [SORT_MODES[getters.sortMode].key(after), after["_id"]]; q.search_after = [SORT_MODES[sortMode].key(after), after["_id"]];
} }
if (getters.optHighlight) { if (getters.optHighlight && !getters.embedding) {
q.highlight = { q.highlight = {
pre_tags: ["<mark>"], pre_tags: ["<mark>"],
post_tags: ["</mark>"], post_tags: ["</mark>"],
@@ -219,7 +259,7 @@ class Sist2Query {
} }
} }
if (getters.sortMode === "random") { if (sortMode === "random") {
q.query = { q.query = {
function_score: { function_score: {
query: { query: {
@@ -249,4 +289,5 @@ class Sist2Query {
} }
} }
export default new Sist2Query();
export default new Sist2ElasticsearchQuery();

View File

@@ -0,0 +1,121 @@
import store from "./store";
import {EsHit, Index} from "@/Sist2Api";
const SORT_MODES = {
score: {
"sort": "score",
},
random: {
"sort": "random"
},
dateAsc: {
"sort": "mtime"
},
dateDesc: {
"sort": "mtime",
"sortAsc": false
},
sizeAsc: {
"sort": "size",
},
sizeDesc: {
"sort": "size",
"sortAsc": false
},
nameAsc: {
"sort": "name",
},
nameDesc: {
"sort": "name",
"sortAsc": false
}
} as any;
interface SortMode {
text: string
mode: any[]
key: (hit: EsHit) => any
}
class Sist2ElasticsearchQuery {
searchQuery(): any {
const getters = store.getters;
const searchText = getters.searchText;
const pathText = getters.pathText;
const sizeMin = getters.sizeMin;
const sizeMax = getters.sizeMax;
const dateMin = getters.dateMin;
const dateMax = getters.dateMax;
const size = getters.size;
const after = getters.lastDoc;
const selectedIndexIds = getters.selectedIndices.map((idx: Index) => idx.id)
const selectedMimeTypes = getters.selectedMimeTypes;
const selectedTags = getters.selectedTags;
const q = {
"pageSize": size
}
Object.assign(q, SORT_MODES[getters.sortMode]);
if (!after) {
q["fetchAggregations"] = true;
}
if (searchText) {
q["query"] = searchText;
}
if (pathText) {
q["path"] = pathText.endsWith("/") ? pathText.slice(0, -1) : pathText;
}
if (sizeMin) {
q["sizeMin"] = sizeMin;
}
if (sizeMax) {
q["sizeMax"] = sizeMax;
}
if (dateMin) {
q["dateMin"] = dateMin;
}
if (dateMax) {
q["dateMax"] = dateMax;
}
if (after) {
q["after"] = after.sort;
}
if (selectedIndexIds.length > 0) {
q["indexIds"] = selectedIndexIds;
}
if (selectedMimeTypes.length > 0) {
q["mimeTypes"] = selectedMimeTypes;
}
if (selectedTags.length > 0) {
q["tags"] = selectedTags
}
if (getters.sortMode == "random") {
q["seed"] = getters.seed;
}
if (getters.optHighlight) {
q["highlight"] = true;
q["highlightContextSize"] = Number(getters.optFragmentSize);
}
if (getters.embedding) {
q["model"] = getters.embeddingsModel;
q["embedding"] = getters.embedding;
q["sort"] = "embedding";
q["sortAsc"] = false;
} else if (getters.sortMode == "embedding") {
q["sort"] = "sort"
q["sortAsc"] = true;
}
return q;
}
}
export default new Sist2ElasticsearchQuery();

View File

@@ -0,0 +1,21 @@
<template>
<span :style="getStyle()">{{span.text}}</span>
</template>
<script>
import ModelsRepo from "@/ml/modelsRepo";
export default {
name: "AnalyzedContentSpan",
props: ["span", "text"],
methods: {
getStyle() {
return ModelsRepo.data[this.$store.getters.nerModel.name].labelStyles[this.span.label];
}
}
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,75 @@
<template>
<div>
<b-card class="mb-2">
<AnalyzedContentSpan v-for="span of legend" :key="span.id" :span="span"
class="mr-2"></AnalyzedContentSpan>
</b-card>
<div class="content-div">
<AnalyzedContentSpan v-for="span of mergedSpans" :key="span.id" :span="span"></AnalyzedContentSpan>
</div>
</div>
</template>
<script>
import AnalyzedContentSpan from "@/components/AnalyzedContentSpan.vue";
import ModelsRepo from "@/ml/modelsRepo";
export default {
name: "AnalyzedContentSpanContainer",
components: {AnalyzedContentSpan},
props: ["spans", "text"],
computed: {
legend() {
return Object.entries(ModelsRepo.data[this.$store.state.nerModel.name].legend)
.map(([label, name]) => ({
text: name,
id: label,
label: label
}));
},
mergedSpans() {
const spans = this.spans;
const merged = [];
let lastLabel = null;
let fixSpace = false;
for (let i = 0; i < spans.length; i++) {
if (spans[i].label !== lastLabel) {
let start = spans[i].wordIndex;
const nextSpan = spans.slice(i + 1).find(s => s.label !== spans[i].label)
let end = nextSpan ? nextSpan.wordIndex : undefined;
if (end !== undefined && this.text[end - 1] === " ") {
end -= 1;
fixSpace = true;
}
merged.push({
text: this.text.slice(start, end),
label: spans[i].label,
id: spans[i].wordIndex
});
if (fixSpace) {
merged.push({
text: " ",
label: "O",
id: end
});
fixSpace = false;
}
lastLabel = spans[i].label;
}
}
return merged;
},
},
}
</script>
<style scoped></style>

View File

@@ -120,7 +120,7 @@ export default {
update(indexId) { update(indexId) {
const svg = d3.select("#date-histogram"); const svg = d3.select("#date-histogram");
d3.csv(Sist2Api.getDateCsv(indexId)).then(tabularData => { d3.json(Sist2Api.getDateStat(indexId)).then(tabularData => {
dateHistogram(tabularData.slice(), svg, this.$t("d3.dateHistogram")); dateHistogram(tabularData.slice(), svg, this.$t("d3.dateHistogram"));
}); });
} }

View File

@@ -91,7 +91,7 @@ export default {
const mimeSvgCount = d3.select("#agg-mime-count"); const mimeSvgCount = d3.select("#agg-mime-count");
const fillOpacity = this.$store.state.optTheme === "black" ? 0.9 : 0.6; const fillOpacity = this.$store.state.optTheme === "black" ? 0.9 : 0.6;
d3.csv(Sist2Api.getMimeCsvUrl(indexId)).then(tabularData => { d3.json(Sist2Api.getMimeStat(indexId)).then(tabularData => {
mimeBarCount(tabularData.slice(), mimeSvgCount, fillOpacity, this.$t("d3.mimeCount")); mimeBarCount(tabularData.slice(), mimeSvgCount, fillOpacity, this.$t("d3.mimeCount"));
}); });
} }

View File

@@ -90,7 +90,7 @@ export default {
const mimeSvgSize = d3.select("#agg-mime-size"); const mimeSvgSize = d3.select("#agg-mime-size");
const fillOpacity = this.$store.state.optTheme === "black" ? 0.9 : 0.6; const fillOpacity = this.$store.state.optTheme === "black" ? 0.9 : 0.6;
d3.csv(Sist2Api.getMimeCsvUrl(indexId)).then(tabularData => { d3.json(Sist2Api.getMimeStat(indexId)).then(tabularData => {
mimeBarSize(tabularData.slice(), mimeSvgSize, fillOpacity, this.$t("d3.mimeSize")); mimeBarSize(tabularData.slice(), mimeSvgSize, fillOpacity, this.$t("d3.mimeSize"));
}); });
} }

View File

@@ -117,7 +117,7 @@ export default {
update(indexId) { update(indexId) {
const svg = d3.select("#size-histogram"); const svg = d3.select("#size-histogram");
d3.csv(Sist2Api.getSizeCsv(indexId)).then(tabularData => { d3.json(Sist2Api.getSizeStat(indexId)).then(tabularData => {
sizeHistogram(tabularData.slice(), svg, this.$t("d3.sizeHistogram")); sizeHistogram(tabularData.slice(), svg, this.$t("d3.sizeHistogram"));
}); });
} }

View File

@@ -1,10 +1,10 @@
<template> <template>
<div> <div>
<b-btn style="float:right;margin-bottom: 10px" @click="downloadTreemap()" variant="primary"> <b-btn style="float:right;margin-bottom: 10px" @click="downloadTreemap()" variant="primary">
{{ $t("download") }} {{ $t("download") }}
</b-btn> </b-btn>
<svg id="treemap"></svg> <svg id="treemap"></svg>
</div> </div>
</template> </template>
<script> <script>
@@ -16,252 +16,252 @@ import domtoimage from "dom-to-image";
const TILING_MODES = { const TILING_MODES = {
"squarify": d3.treemapSquarify, "squarify": d3.treemapSquarify,
"binary": d3.treemapBinary, "binary": d3.treemapBinary,
"sliceDice": d3.treemapSliceDice, "sliceDice": d3.treemapSliceDice,
"slice": d3.treemapSlice, "slice": d3.treemapSlice,
"dice": d3.treemapDice, "dice": d3.treemapDice,
}; };
const COLORS = { const COLORS = {
"PuBuGn": d3.interpolatePuBuGn, "PuBuGn": d3.interpolatePuBuGn,
"PuRd": d3.interpolatePuRd, "PuRd": d3.interpolatePuRd,
"PuBu": d3.interpolatePuBu, "PuBu": d3.interpolatePuBu,
"YlOrBr": d3.interpolateYlOrBr, "YlOrBr": d3.interpolateYlOrBr,
"YlOrRd": d3.interpolateYlOrRd, "YlOrRd": d3.interpolateYlOrRd,
"YlGn": d3.interpolateYlGn, "YlGn": d3.interpolateYlGn,
"YlGnBu": d3.interpolateYlGnBu, "YlGnBu": d3.interpolateYlGnBu,
"Plasma": d3.interpolatePlasma, "Plasma": d3.interpolatePlasma,
"Magma": d3.interpolateMagma, "Magma": d3.interpolateMagma,
"Inferno": d3.interpolateInferno, "Inferno": d3.interpolateInferno,
"Viridis": d3.interpolateViridis, "Viridis": d3.interpolateViridis,
"Turbo": d3.interpolateTurbo, "Turbo": d3.interpolateTurbo,
}; };
const SIZES = { const SIZES = {
"small": [800, 600], "small": [800, 600],
"medium": [1300, 750], "medium": [1300, 750],
"large": [1900, 900], "large": [1900, 900],
"x-large": [2800, 1700], "x-large": [2800, 1700],
"xx-large": [3600, 2000], "xx-large": [3600, 2000],
}; };
const uids = {}; const uids = {};
function uid(name) { function uid(name) {
let id = uids[name] || 0; let id = uids[name] || 0;
uids[name] = id + 1; uids[name] = id + 1;
return name + id; return name + id;
} }
function cascade(root, offset) { function cascade(root, offset) {
const x = new Map; const x = new Map;
const y = new Map; const y = new Map;
return root.eachAfter(d => { return root.eachAfter(d => {
if (d.children && d.children.length !== 0) { if (d.children && d.children.length !== 0) {
x.set(d, 1 + d3.max(d.children, c => c.x1 === d.x1 - offset ? x.get(c) : NaN)); x.set(d, 1 + d3.max(d.children, c => c.x1 === d.x1 - offset ? x.get(c) : NaN));
y.set(d, 1 + d3.max(d.children, c => c.y1 === d.y1 - offset ? y.get(c) : NaN)); y.set(d, 1 + d3.max(d.children, c => c.y1 === d.y1 - offset ? y.get(c) : NaN));
} else { } else {
x.set(d, 0); x.set(d, 0);
y.set(d, 0); y.set(d, 0);
} }
}).eachBefore(d => { }).eachBefore(d => {
d.x1 -= 2 * offset * x.get(d); d.x1 -= 2 * offset * x.get(d);
d.y1 -= 2 * offset * y.get(d); d.y1 -= 2 * offset * y.get(d);
}); });
} }
function cascadeTreemap(data, svg, width, height, tilingMode, treemapColor) { function cascadeTreemap(data, svg, width, height, tilingMode, treemapColor) {
const root = cascade( const root = cascade(
d3.treemap() d3.treemap()
.size([width, height]) .size([width, height])
.tile(TILING_MODES[tilingMode]) .tile(TILING_MODES[tilingMode])
.paddingOuter(3) .paddingOuter(3)
.paddingTop(16) .paddingTop(16)
.paddingInner(1) .paddingInner(1)
.round(true)( .round(true)(
d3.hierarchy(data) d3.hierarchy(data)
.sum(d => d.value) .sum(d => d.value)
.sort((a, b) => b.value - a.value) .sort((a, b) => b.value - a.value)
), ),
3 // treemap.paddingOuter 3 // treemap.paddingOuter
); );
const maxDepth = Math.max(...root.descendants().map(d => d.depth)); const maxDepth = Math.max(...root.descendants().map(d => d.depth));
const color = d3.scaleSequential([maxDepth, -1], COLORS[treemapColor]); const color = d3.scaleSequential([maxDepth, -1], COLORS[treemapColor]);
svg.append("filter") svg.append("filter")
.attr("id", "shadow") .attr("id", "shadow")
.append("feDropShadow") .append("feDropShadow")
.attr("flood-opacity", 0.3) .attr("flood-opacity", 0.3)
.attr("dx", 0) .attr("dx", 0)
.attr("stdDeviation", 3); .attr("stdDeviation", 3);
const node = svg.selectAll("g") const node = svg.selectAll("g")
.data( .data(
d3.nest() d3.nest()
.key(d => d.depth).sortKeys(d3.ascending) .key(d => d.depth).sortKeys(d3.ascending)
.entries(root.descendants()) .entries(root.descendants())
) )
.join("g") .join("g")
.attr("filter", "url(#shadow)") .attr("filter", "url(#shadow)")
.selectAll("g") .selectAll("g")
.data(d => d.values) .data(d => d.values)
.join("g") .join("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`); .attr("transform", d => `translate(${d.x0},${d.y0})`);
node.append("title") node.append("title")
.text(d => `${d.ancestors().reverse().splice(1).map(d => d.data.name).join("/")}\n${humanFileSize(d.value)}`); .text(d => `${d.ancestors().reverse().splice(1).map(d => d.data.name).join("/")}\n${humanFileSize(d.value)}`);
node.append("rect") node.append("rect")
.attr("id", d => (d.nodeUid = uid("node"))) .attr("id", d => (d.nodeUid = uid("node")))
.attr("fill", d => color(d.depth)) .attr("fill", d => color(d.depth))
.attr("width", d => d.x1 - d.x0) .attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0); .attr("height", d => d.y1 - d.y0);
node.append("clipPath") node.append("clipPath")
.attr("id", d => (d.clipUid = uid("clip"))) .attr("id", d => (d.clipUid = uid("clip")))
.append("use") .append("use")
.attr("href", d => `#${d.nodeUid}`); .attr("href", d => `#${d.nodeUid}`);
node.append("text") node.append("text")
.attr("fill", d => d3.hsl(color(d.depth)).l > .5 ? "#333" : "#eee") .attr("fill", d => d3.hsl(color(d.depth)).l > .5 ? "#333" : "#eee")
.attr("clip-path", d => `url(#${d.clipUid})`) .attr("clip-path", d => `url(#${d.clipUid})`)
.selectAll("tspan") .selectAll("tspan")
.data(d => [d.data.name, humanFileSize(d.value)]) .data(d => [d.data.name, humanFileSize(d.value)])
.join("tspan") .join("tspan")
.text(d => d); .text(d => d);
node.filter(d => d.children).selectAll("tspan") node.filter(d => d.children).selectAll("tspan")
.attr("dx", 3) .attr("dx", 3)
.attr("y", 13); .attr("y", 13);
node.filter(d => !d.children).selectAll("tspan") node.filter(d => !d.children).selectAll("tspan")
.attr("x", 3) .attr("x", 3)
.attr("y", (d, i) => `${i === 0 ? 1.1 : 2.3}em`); .attr("y", (d, i) => `${i === 0 ? 1.1 : 2.3}em`);
} }
function flatTreemap(data, svg, width, height, groupingDepth, tilingMode, fillOpacity) { function flatTreemap(data, svg, width, height, groupingDepth, tilingMode, fillOpacity) {
const ordinalColor = d3.scaleOrdinal(d3.schemeCategory10); const ordinalColor = d3.scaleOrdinal(d3.schemeCategory10);
const root = d3.treemap() const root = d3.treemap()
.tile(TILING_MODES[tilingMode]) .tile(TILING_MODES[tilingMode])
.size([width, height]) .size([width, height])
.padding(1) .padding(1)
.round(true)( .round(true)(
d3.hierarchy(data) d3.hierarchy(data)
.sum(d => d.value) .sum(d => d.value)
.sort((a, b) => b.value - a.value) .sort((a, b) => b.value - a.value)
); );
const leaf = svg.selectAll("g") const leaf = svg.selectAll("g")
.data(root.leaves()) .data(root.leaves())
.join("g") .join("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`); .attr("transform", d => `translate(${d.x0},${d.y0})`);
leaf.append("title") leaf.append("title")
.text(d => `${d.ancestors().reverse().map(d => d.data.name).join("/")}\n${humanFileSize(d.value)}`); .text(d => `${d.ancestors().reverse().map(d => d.data.name).join("/")}\n${humanFileSize(d.value)}`);
leaf.append("rect") leaf.append("rect")
.attr("id", d => (d.leafUid = uid("leaf"))) .attr("id", d => (d.leafUid = uid("leaf")))
.attr("fill", d => { .attr("fill", d => {
while (d.depth > groupingDepth) d = d.parent; while (d.depth > groupingDepth) d = d.parent;
return ordinalColor(d.data.name); return ordinalColor(d.data.name);
}) })
.attr("fill-opacity", fillOpacity) .attr("fill-opacity", fillOpacity)
.attr("width", d => d.x1 - d.x0) .attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0); .attr("height", d => d.y1 - d.y0);
leaf.append("clipPath") leaf.append("clipPath")
.attr("id", d => (d.clipUid = uid("clip"))) .attr("id", d => (d.clipUid = uid("clip")))
.append("use") .append("use")
.attr("href", d => `#${d.leafUid}`); .attr("href", d => `#${d.leafUid}`);
leaf.append("text") leaf.append("text")
.attr("clip-path", d => `url(#${d.clipUid})`) .attr("clip-path", d => `url(#${d.clipUid})`)
.selectAll("tspan") .selectAll("tspan")
.data(d => { .data(d => {
if (d.data.name === ".") { if (d.data.name === ".") {
d = d.parent; d = d.parent;
} }
return [d.data.name, humanFileSize(d.value)] return [d.data.name, humanFileSize(d.value)]
}) })
.join("tspan") .join("tspan")
.attr("x", 2) .attr("x", 2)
.attr("y", (d, i) => `${i === 0 ? 1.1 : 2.3}em`) .attr("y", (d, i) => `${i === 0 ? 1.1 : 2.3}em`)
.text(d => d); .text(d => d);
} }
function exportTreemap(indexName, width, height) { function exportTreemap(indexName, width, height) {
domtoimage.toBlob(document.getElementById("treemap"), {width: width, height: height}) domtoimage.toBlob(document.getElementById("treemap"), {width: width, height: height})
.then(function (blob) { .then(function (blob) {
let a = document.createElement("a"); let a = document.createElement("a");
let url = URL.createObjectURL(blob); let url = URL.createObjectURL(blob);
a.href = url; a.href = url;
a.download = `${indexName}_treemap.png`; a.download = `${indexName}_treemap.png`;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
setTimeout(function () { setTimeout(function () {
document.body.removeChild(a); document.body.removeChild(a);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}, 0); }, 0);
}); });
} }
export default { export default {
name: "D3Treemap", name: "D3Treemap",
props: ["indexId"], props: ["indexId"],
watch: { watch: {
indexId: function () { indexId: function () {
this.update(this.indexId); this.update(this.indexId);
}
},
mounted() {
this.update(this.indexId);
},
methods: {
update(indexId) {
const width = SIZES[this.$store.state.optTreemapSize][0];
const height = SIZES[this.$store.state.optTreemapSize][1];
const tilingMode = this.$store.state.optTreemapTiling;
const groupingDepth = this.$store.state.optTreemapColorGroupingDepth;
const treemapColor = this.$store.state.optTreemapColor;
const treemapType = this.$store.state.optTreemapType;
const treemapSvg = d3.select("#treemap");
treemapSvg.selectAll("*").remove();
treemapSvg.attr("viewBox", [0, 0, width, height])
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
.attr("version", "1.1")
.style("overflow", "visible")
.style("font", "10px sans-serif");
d3.csv(Sist2Api.getTreemapCsvUrl(indexId)).then(tabularData => {
tabularData.forEach(row => {
row.taxonomy = row.path.split("/");
row.size = Number(row.size);
});
if (treemapType === "cascaded") {
const data = burrow(tabularData, false);
cascadeTreemap(data, treemapSvg, width, height, tilingMode, treemapColor);
} else {
const data = burrow(tabularData.sort((a, b) => b.taxonomy.length - a.taxonomy.length), true);
const fillOpacity = this.$store.state.optTheme === "black" ? 0.9 : 0.6;
flatTreemap(data, treemapSvg, width, height, groupingDepth, tilingMode, fillOpacity);
} }
});
}, },
downloadTreemap() { mounted() {
const width = SIZES[this.$store.state.optTreemapSize][0]; this.update(this.indexId);
const height = SIZES[this.$store.state.optTreemapSize][1]; },
methods: {
update(indexId) {
const width = SIZES[this.$store.state.optTreemapSize][0];
const height = SIZES[this.$store.state.optTreemapSize][1];
const tilingMode = this.$store.state.optTreemapTiling;
const groupingDepth = this.$store.state.optTreemapColorGroupingDepth;
const treemapColor = this.$store.state.optTreemapColor;
const treemapType = this.$store.state.optTreemapType;
exportTreemap(this.indexId, width, height); const treemapSvg = d3.select("#treemap");
treemapSvg.selectAll("*").remove();
treemapSvg.attr("viewBox", [0, 0, width, height])
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
.attr("version", "1.1")
.style("overflow", "visible")
.style("font", "10px sans-serif");
d3.json(Sist2Api.getTreemapStat(indexId)).then(tabularData => {
tabularData.forEach(row => {
row.taxonomy = row.path.split("/");
row.size = Number(row.size);
});
if (treemapType === "cascaded") {
const data = burrow(tabularData, false);
cascadeTreemap(data, treemapSvg, width, height, tilingMode, treemapColor);
} else {
const data = burrow(tabularData.sort((a, b) => b.taxonomy.length - a.taxonomy.length), true);
const fillOpacity = this.$store.state.optTheme === "black" ? 0.9 : 0.6;
flatTreemap(data, treemapSvg, width, height, groupingDepth, tilingMode, fillOpacity);
}
});
},
downloadTreemap() {
const width = SIZES[this.$store.state.optTreemapSize][0];
const height = SIZES[this.$store.state.optTreemapSize][1];
exportTreemap(this.indexId, width, height);
}
} }
}
} }
</script> </script>

View File

@@ -1,41 +1,57 @@
<template> <template>
<b-card class="mb-4 mt-4"> <b-card v-if="$store.state.sist2Info.showDebugInfo" class="mb-4 mt-4">
<b-card-title><DebugIcon class="mr-1"></DebugIcon>{{ $t("debug") }}</b-card-title> <b-card-title>
<p v-html="$t('debugDescription')"></p> <DebugIcon class="mr-1"></DebugIcon>
{{ $t("debug") }}
</b-card-title>
<p v-html="$t('debugDescription')"></p>
<b-card-body> <b-card-body>
<b-table :items="tableItems" small borderless responsive="md" thead-class="hidden" class="mb-0"></b-table> <b-table :items="tableItems" small borderless responsive="md" thead-class="hidden" class="mb-0"></b-table>
<hr /> <hr/>
<IndexDebugInfo v-for="idx of $store.state.sist2Info.indices" :key="idx.id" :index="idx" class="mt-2"></IndexDebugInfo> <IndexDebugInfo v-for="idx of $store.state.sist2Info.indices" :key="idx.id" :index="idx"
</b-card-body> class="mt-2"></IndexDebugInfo>
</b-card> </b-card-body>
</b-card>
</template> </template>
<script> <script>
import IndexDebugInfo from "@/components/IndexDebugInfo"; import IndexDebugInfo from "@/components/IndexDebugInfo";
import DebugIcon from "@/components/icons/DebugIcon"; import DebugIcon from "@/components/icons/DebugIcon";
import {mapGetters} from "vuex";
export default { export default {
name: "DebugInfo.vue", name: "DebugInfo.vue",
components: {DebugIcon, IndexDebugInfo}, components: {DebugIcon, IndexDebugInfo},
computed: { computed: {
tableItems() { ...mapGetters([
return [ "uiSqliteMode",
{key: "version", value: this.$store.state.sist2Info.version}, ]),
{key: "platform", value: this.$store.state.sist2Info.platform}, tableItems() {
{key: "debugBinary", value: this.$store.state.sist2Info.debug}, const items = [
{key: "sist2CommitHash", value: this.$store.state.sist2Info.sist2Hash}, {key: "version", value: this.$store.state.sist2Info.version},
{key: "esIndex", value: this.$store.state.sist2Info.esIndex}, {key: "platform", value: this.$store.state.sist2Info.platform},
{key: "tagline", value: this.$store.state.sist2Info.tagline}, {key: "debugBinary", value: this.$store.state.sist2Info.debug},
{key: "dev", value: this.$store.state.sist2Info.dev}, {key: "sist2CommitHash", value: this.$store.state.sist2Info.sist2Hash},
{key: "mongooseVersion", value: this.$store.state.sist2Info.mongooseVersion}, {key: "esIndex", value: this.$store.state.sist2Info.esIndex},
{key: "esVersion", value: this.$store.state.sist2Info.esVersion}, {key: "tagline", value: this.$store.state.sist2Info.tagline},
{key: "esVersionSupported", value: this.$store.state.sist2Info.esVersionSupported}, {key: "dev", value: this.$store.state.sist2Info.dev},
{key: "esVersionLegacy", value: this.$store.state.sist2Info.esVersionLegacy}, {key: "mongooseVersion", value: this.$store.state.sist2Info.mongooseVersion},
] ];
if (!this.uiSqliteMode) {
items.push(
{key: "esVersion", value: this.$store.state.sist2Info.esVersion},
{key: "esVersionSupported", value: this.$store.state.sist2Info.esVersionSupported},
{key: "esVersionLegacy", value: this.$store.state.sist2Info.esVersionLegacy},
{key: "esVersionHasKnn", value: this.$store.state.sist2Info.esVersionHasKnn},
);
}
return items;
}
} }
}
} }
</script> </script>

View File

@@ -1,44 +1,45 @@
<template> <template>
<div class="doc-card" :class="{'sub-document': doc._props.isSubDocument}" :style="`width: ${width}px`" <div class="doc-card" :class="{'sub-document': doc._props.isSubDocument}" :style="`width: ${width}px`"
@click="$store.commit('busTnTouchStart', null)"> @click="$store.commit('busTnTouchStart', null)">
<b-card <b-card
no-body no-body
img-top img-top
> >
<!-- Info modal--> <!-- Info modal-->
<DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal> <DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal>
<ContentDiv :doc="doc"></ContentDiv> <ContentDiv :doc="doc"></ContentDiv>
<!-- Thumbnail--> <!-- Thumbnail-->
<FullThumbnail :doc="doc" :small-badge="smallBadge" @onThumbnailClick="onThumbnailClick()"></FullThumbnail> <FullThumbnail :doc="doc" :small-badge="smallBadge" @onThumbnailClick="onThumbnailClick()"></FullThumbnail>
<!-- Audio player--> <!-- Audio player-->
<audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls <audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls
:type="doc._source.mime" :type="doc._source.mime"
:src="`f/${doc._id}`" :src="`f/${doc._source.index}/${doc._id}`"
@play="onAudioPlay()"></audio> @play="onAudioPlay()"></audio>
<b-card-body class="padding-03"> <b-card-body class="padding-03">
<!-- Title line --> <!-- Title line -->
<div style="display: flex"> <div style="display: flex">
<span class="info-icon" @click="onInfoClick()"></span> <span class="info-icon" @click="onInfoClick()"></span>
<DocFileTitle :doc="doc"></DocFileTitle> <MLIcon v-if="doc._source.embedding" clickable @click="onEmbeddingClick()"></MLIcon>
</div> <DocFileTitle :doc="doc"></DocFileTitle>
</div>
<!-- Featured line --> <!-- Featured line -->
<div style="display: flex"> <div style="display: flex">
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine> <FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
</div> </div>
<!-- Tags --> <!-- Tags -->
<div class="card-text"> <div class="card-text">
<TagContainer :hit="doc"></TagContainer> <TagContainer :hit="doc"></TagContainer>
</div> </div>
</b-card-body> </b-card-body>
</b-card> </b-card>
</div> </div>
</template> </template>
<script> <script>
@@ -49,94 +50,103 @@ import DocInfoModal from "@/components/DocInfoModal.vue";
import ContentDiv from "@/components/ContentDiv.vue"; import ContentDiv from "@/components/ContentDiv.vue";
import FullThumbnail from "@/components/FullThumbnail"; import FullThumbnail from "@/components/FullThumbnail";
import FeaturedFieldsLine from "@/components/FeaturedFieldsLine"; import FeaturedFieldsLine from "@/components/FeaturedFieldsLine";
import MLIcon from "@/components/icons/MlIcon.vue";
import Sist2Api from "@/Sist2Api";
export default { export default {
components: {FeaturedFieldsLine, FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer}, components: {MLIcon, FeaturedFieldsLine, FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer},
props: ["doc", "width"], props: ["doc", "width"],
data() { data() {
return { return {
ext: ext, ext: ext,
showInfo: false, showInfo: false,
}
},
computed: {
smallBadge() {
return this.width < 150;
}
},
methods: {
humanFileSize: humanFileSize,
humanTime: humanTime,
onInfoClick() {
this.showInfo = true;
},
async onThumbnailClick() {
this.$store.commit("setUiLightboxSlide", this.doc._seq);
await this.$store.dispatch("showLightbox");
},
onAudioPlay() {
document.getElementsByTagName("audio").forEach((el) => {
if (el !== this.$refs["audio"]) {
el.pause();
} }
});
}, },
}, computed: {
smallBadge() {
return this.width < 150;
}
},
methods: {
humanFileSize: humanFileSize,
humanTime: humanTime,
onInfoClick() {
this.showInfo = true;
},
onEmbeddingClick() {
Sist2Api.getEmbeddings(this.doc._source.index, this.doc._id, this.$store.state.embeddingsModel).then(embeddings => {
this.$store.commit("setEmbeddingText", "");
this.$store.commit("setEmbedding", embeddings);
this.$store.commit("setEmbeddingDoc", this.doc);
})
},
async onThumbnailClick() {
this.$store.commit("setUiLightboxSlide", this.doc._seq);
await this.$store.dispatch("showLightbox");
},
onAudioPlay() {
Array.prototype.slice.call(document.getElementsByTagName("audio")).forEach((el) => {
if (el !== this.$refs["audio"]) {
el.pause();
}
});
},
},
} }
</script> </script>
<style> <style>
.fit { .fit {
display: block; display: block;
min-width: 64px; min-width: 64px;
max-width: 100%; max-width: 100%;
/*max-height: 400px;*/ /*max-height: 400px;*/
margin: 0 auto 0; margin: 0 auto 0;
width: auto; width: auto;
height: auto; height: auto;
} }
.audio-fit { .audio-fit {
height: 39px; height: 39px;
vertical-align: bottom; vertical-align: bottom;
display: inline; display: inline;
width: 100%; width: 100%;
} }
</style> </style>
<style scoped> <style scoped>
.padding-03 { .padding-03 {
padding: 0.3rem; padding: 0.3rem;
} }
.card { .card {
margin-top: 1em; margin-top: 1em;
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important; box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important;
border-radius: 0; border-radius: 0;
border: none; border: none;
} }
.card-body { .card-body {
padding: 0.3rem; padding: 0.3rem;
} }
.doc-card { .doc-card {
padding-left: 3px; padding-left: 3px;
padding-right: 3px; padding-right: 3px;
} }
.sub-document .card { .sub-document .card {
background: #AB47BC1F !important; background: #AB47BC1F !important;
} }
.theme-black .sub-document .card { .theme-black .sub-document .card {
background: #37474F !important; background: #37474F !important;
} }
.sub-document .fit { .sub-document .fit {
padding: 4px 4px 0 4px; padding: 4px 4px 0 4px;
} }
</style> </style>

View File

@@ -1,63 +1,70 @@
<template> <template>
<a :href="`f/${doc._id}`" class="file-title-anchor" target="_blank"> <a :href="`f/${doc._source.index}/${doc._id}`"
<div class="file-title" :title="doc._source.path + '/' + doc._source.name + ext(doc)" :class="doc._source.embedding ? 'file-title-anchor-with-embedding' : 'file-title-anchor'" target="_blank">
v-html="fileName() + ext(doc)"></div> <div class="file-title" :title="doc._source.path + '/' + doc._source.name + ext(doc)"
</a> v-html="fileName() + ext(doc)"></div>
</a>
</template> </template>
<script> <script>
import {ext} from "@/util"; import {ext} from "@/util";
export default { export default {
name: "DocFileTitle", name: "DocFileTitle",
props: ["doc"], props: ["doc"],
methods: { methods: {
ext: ext, ext: ext,
fileName() { fileName() {
if (!this.doc.highlight) { if (!this.doc.highlight) {
return this.doc._source.name; return this.doc._source.name;
} }
if (this.doc.highlight["name.nGram"]) { if (this.doc.highlight["name.nGram"]) {
return this.doc.highlight["name.nGram"]; return this.doc.highlight["name.nGram"];
} }
if (this.doc.highlight.name) { if (this.doc.highlight.name) {
return this.doc.highlight.name; return this.doc.highlight.name;
} }
return this.doc._source.name; return this.doc._source.name;
}
} }
}
} }
</script> </script>
<style scoped> <style scoped>
.file-title-anchor { .file-title-anchor {
max-width: calc(100% - 1.2rem); max-width: calc(100% - 1.2rem);
}
.file-title-anchor-with-embedding {
max-width: calc(100% - 2.2rem);
} }
.file-title { .file-title {
width: 100%; width: 100%;
line-height: 1rem; max-width: 100%;
height: 1.1rem; line-height: 1rem;
white-space: nowrap; height: 1.1rem;
text-overflow: ellipsis; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
font-size: 16px; overflow: hidden;
font-family: "Source Sans Pro", sans-serif; font-size: 16px;
font-weight: bold; font-family: "Source Sans Pro", sans-serif;
font-weight: bold;
} }
.theme-black .file-title { .theme-black .file-title {
color: #ddd; color: #ddd;
} }
.theme-black .file-title:hover { .theme-black .file-title:hover {
color: #fff; color: #fff;
} }
.theme-light .file-title { .theme-light .file-title {
color: black; color: black;
} }
.doc-card .file-title { .doc-card .file-title {
font-size: 12px; font-size: 12px;
} }
</style> </style>

View File

@@ -1,63 +1,64 @@
<template> <template>
<b-list-group-item class="flex-column align-items-start mb-2" :class="{'sub-document': doc._props.isSubDocument}" <b-list-group-item class="flex-column align-items-start mb-2" :class="{'sub-document': doc._props.isSubDocument}"
@mouseenter="onTnEnter()" @mouseleave="onTnLeave()"> @mouseenter="onTnEnter()" @mouseleave="onTnLeave()">
<!-- Info modal--> <!-- Info modal-->
<DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal> <DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal>
<div class="media ml-2"> <div class="media ml-2">
<!-- Thumbnail--> <!-- Thumbnail-->
<div v-if="doc._props.hasThumbnail" class="align-self-start mr-2 wrapper-sm"> <div v-if="doc._props.hasThumbnail" class="align-self-start mr-2 wrapper-sm">
<div class="img-wrapper"> <div class="img-wrapper">
<div v-if="doc._props.isPlayableVideo" class="play"> <div v-if="doc._props.isPlayableVideo" class="play">
<svg viewBox="0 0 494.942 494.942" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 494.942 494.942" xmlns="http://www.w3.org/2000/svg">
<path d="m35.353 0 424.236 247.471-424.236 247.471z"/> <path d="m35.353 0 424.236 247.471-424.236 247.471z"/>
</svg> </svg>
</div> </div>
<img v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo" <img v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo"
:src="(doc._props.isGif && hover) ? `f/${doc._id}` : `t/${doc._source.index}/${doc._id}`" :src="(doc._props.isGif && hover) ? `f/${doc._source.index}/${doc._id}` : `t/${doc._source.index}/${doc._id}`"
alt="" alt=""
class="pointer fit-sm" @click="onThumbnailClick()"> class="pointer fit-sm" @click="onThumbnailClick()">
<img v-else :src="`t/${doc._source.index}/${doc._id}`" alt="" <img v-else :src="`t/${doc._source.index}/${doc._id}`" alt=""
class="fit-sm"> class="fit-sm">
</div> </div>
</div> </div>
<div v-else class="file-icon-wrapper" style=""> <div v-else class="file-icon-wrapper" style="">
<FileIcon></FileIcon> <FileIcon></FileIcon>
</div> </div>
<!-- Doc line--> <!-- Doc line-->
<div class="doc-line ml-3"> <div class="doc-line ml-3">
<div style="display: flex"> <div style="display: flex">
<span class="info-icon" @click="showInfo = true"></span> <span class="info-icon" @click="showInfo = true"></span>
<DocFileTitle :doc="doc"></DocFileTitle> <MLIcon v-if="doc._source.embedding" clickable @click="onEmbeddingClick()"></MLIcon>
</div> <DocFileTitle :doc="doc"></DocFileTitle>
</div>
<!-- Content highlight --> <!-- Content highlight -->
<ContentDiv :doc="doc"></ContentDiv> <ContentDiv :doc="doc"></ContentDiv>
<div class="path-row"> <div class="path-row">
<div class="path-line" v-html="path()"></div> <div class="path-line" v-html="path()"></div>
<TagContainer :hit="doc"></TagContainer> <TagContainer :hit="doc"></TagContainer>
</div> </div>
<div v-if="doc._source.pages || doc._source.author" class="path-row text-muted"> <div v-if="doc._source.pages || doc._source.author" class="path-row text-muted">
<span v-if="doc._source.pages">{{ doc._source.pages }} {{ <span v-if="doc._source.pages">{{ doc._source.pages }} {{
doc._source.pages > 1 ? $t("pages") : $t("page") doc._source.pages > 1 ? $t("pages") : $t("page")
}}</span> }}</span>
<span v-if="doc._source.author && doc._source.pages" class="mx-1">-</span> <span v-if="doc._source.author && doc._source.pages" class="mx-1">-</span>
<span v-if="doc._source.author">{{ doc._source.author }}</span> <span v-if="doc._source.author">{{ doc._source.author }}</span>
</div> </div>
<!-- Featured line --> <!-- Featured line -->
<div style="display: flex"> <div style="display: flex">
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine> <FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
</div>
</div>
</div> </div>
</div> </b-list-group-item>
</div>
</b-list-group-item>
</template> </template>
<script> <script>
@@ -67,131 +68,140 @@ import DocInfoModal from "@/components/DocInfoModal";
import ContentDiv from "@/components/ContentDiv"; import ContentDiv from "@/components/ContentDiv";
import FileIcon from "@/components/icons/FileIcon"; import FileIcon from "@/components/icons/FileIcon";
import FeaturedFieldsLine from "@/components/FeaturedFieldsLine"; import FeaturedFieldsLine from "@/components/FeaturedFieldsLine";
import MLIcon from "@/components/icons/MlIcon.vue";
import Sist2Api from "@/Sist2Api";
export default { export default {
name: "DocListItem", name: "DocListItem",
components: {FileIcon, ContentDiv, DocInfoModal, DocFileTitle, TagContainer, FeaturedFieldsLine}, components: {MLIcon, FileIcon, ContentDiv, DocInfoModal, DocFileTitle, TagContainer, FeaturedFieldsLine},
props: ["doc"], props: ["doc"],
data() { data() {
return { return {
hover: false, hover: false,
showInfo: false showInfo: false
} }
},
methods: {
async onThumbnailClick() {
this.$store.commit("setUiLightboxSlide", this.doc._seq);
await this.$store.dispatch("showLightbox");
}, },
path() { methods: {
if (!this.doc.highlight) { async onThumbnailClick() {
return this.doc._source.path + "/" this.$store.commit("setUiLightboxSlide", this.doc._seq);
} await this.$store.dispatch("showLightbox");
if (this.doc.highlight["path.text"]) { },
return this.doc.highlight["path.text"] + "/" onEmbeddingClick() {
} Sist2Api.getEmbeddings(this.doc._source.index, this.doc._id, this.$store.state.embeddingsModel).then(embeddings => {
this.$store.commit("setEmbeddingText", "");
this.$store.commit("setEmbedding", embeddings);
this.$store.commit("setEmbeddingDoc", this.doc);
})
},
path() {
if (!this.doc.highlight) {
return this.doc._source.path + "/"
}
if (this.doc.highlight["path.text"]) {
return this.doc.highlight["path.text"] + "/"
}
if (this.doc.highlight["path.nGram"]) { if (this.doc.highlight["path.nGram"]) {
return this.doc.highlight["path.nGram"] + "/" return this.doc.highlight["path.nGram"] + "/"
} }
return this.doc._source.path + "/" return this.doc._source.path + "/"
}, },
onTnEnter() { onTnEnter() {
this.hover = true; this.hover = true;
}, },
onTnLeave() { onTnLeave() {
this.hover = false; this.hover = false;
}, },
} }
} }
</script> </script>
<style scoped> <style scoped>
.sub-document { .sub-document {
background: #AB47BC1F !important; background: #AB47BC1F !important;
} }
.theme-black .sub-document { .theme-black .sub-document {
background: #37474F !important; background: #37474F !important;
} }
.list-group { .list-group {
margin-top: 1em; margin-top: 1em;
} }
.list-group-item { .list-group-item {
padding: .25rem 0.5rem; padding: .25rem 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important; box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important;
border-radius: 0; border-radius: 0;
border: none; border: none;
} }
.path-row { .path-row {
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
-ms-flex-align: start; -ms-flex-align: start;
align-items: flex-start; align-items: flex-start;
} }
.path-line { .path-line {
color: #808080; color: #808080;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
margin-right: 0.3em; margin-right: 0.3em;
} }
.theme-black .path-line { .theme-black .path-line {
color: #bbb; color: #bbb;
} }
.play { .play {
position: absolute; position: absolute;
width: 18px; width: 18px;
height: 18px; height: 18px;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
pointer-events: none; pointer-events: none;
} }
.play svg { .play svg {
fill: rgba(0, 0, 0, 0.7); fill: rgba(0, 0, 0, 0.7);
} }
.list-group-item .img-wrapper { .list-group-item .img-wrapper {
width: 88px; width: 88px;
height: 88px; height: 88px;
position: relative; position: relative;
} }
.fit-sm { .fit-sm {
max-height: 100%; max-height: 100%;
max-width: 100%; max-width: 100%;
width: auto; width: auto;
height: auto; height: auto;
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
margin: auto; margin: auto;
/*box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.12);*/ /*box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.12);*/
} }
.doc-line { .doc-line {
max-width: calc(100% - 88px - 1.5rem); max-width: calc(100% - 88px - 1.5rem);
flex: 1; flex: 1;
vertical-align: middle; vertical-align: middle;
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
} }
.file-icon-wrapper { .file-icon-wrapper {
width: calc(88px + .5rem); width: calc(88px + .5rem);
height: 88px; height: 88px;
position: relative; position: relative;
} }
</style> </style>

View File

@@ -0,0 +1,155 @@
<template>
<div>
<b-progress v-if="modelLoading && [0, 1].includes(modelLoadingProgress)" max="1" class="mb-1" variant="primary"
striped animated :value="1">
</b-progress>
<b-progress v-else-if="modelLoading" :value="modelLoadingProgress" max="1" class="mb-1" variant="warning"
show-progress>
</b-progress>
<div style="display: flex">
<b-select :options="modelOptions()" class="mr-2 input-prepend" :value="modelName"
@change="onModelChange($event)"></b-select>
<b-input-group>
<b-form-input :value="embeddingText"
:placeholder="$store.state.embeddingDoc ? ' ' : $t('embeddingsSearchPlaceholder')"
@input="onInput($event)"
:disabled="modelLoading"
:style="{'pointer-events': $store.state.embeddingDoc ? 'none' : undefined}"
></b-form-input>
<b-badge v-if="$store.state.embeddingDoc" pill variant="primary" class="overlay-badge" href="#"
@click="onBadgeClick()">{{ docName }}
</b-badge>
<template #prepend>
</template>
<template #append>
<b-input-group-text>
<MLIcon class="ml-append" big></MLIcon>
</b-input-group-text>
</template>
</b-input-group>
</div>
</div>
</template>
<script>
import {mapGetters, mapMutations} from "vuex";
import {CLIPTransformerModel} from "@/ml/CLIPTransformerModel"
import _debounce from "lodash/debounce";
import MLIcon from "@/components/icons/MlIcon.vue";
import Sist2AdminApi from "@/Sist2Api";
export default {
components: {MLIcon},
data() {
return {
modelLoading: false,
modelLoadingProgress: 0,
modelLoaded: false,
model: null,
modelName: null
}
},
computed: {
...mapGetters({
optQueryMode: "optQueryMode",
embeddingText: "embeddingText",
fuzzy: "fuzzy",
}),
docName() {
const ext = this.$store.state.embeddingDoc._source.extension;
return this.$store.state.embeddingDoc._source.name +
(ext ? "." + ext : "")
}
},
mounted() {
// Set default model
this.modelName = Sist2AdminApi.models()[0].name;
this.onModelChange(this.modelName);
this.onInput = _debounce(this._onInput, 450, {leading: false});
},
methods: {
...mapMutations({
setEmbeddingText: "setEmbeddingText",
setEmbedding: "setEmbedding",
setEmbeddingModel: "setEmbeddingsModel",
}),
async loadModel() {
this.modelLoading = true;
await this.model.init(async progress => {
this.modelLoadingProgress = progress;
});
this.modelLoading = false;
this.modelLoaded = true;
},
async _onInput(text) {
try {
if (!this.modelLoaded) {
await this.loadModel();
}
if (text.length === 0) {
this.setEmbeddingText("");
this.setEmbedding(null);
return;
}
const embeddings = await this.model.predict(text);
this.setEmbeddingText(text);
this.setEmbedding(embeddings);
} catch (e) {
alert(e)
}
},
modelOptions() {
return Sist2AdminApi.models().map(model => model.name);
},
onModelChange(name) {
this.modelLoaded = false;
this.modelLoadingProgress = 0;
const modelInfo = Sist2AdminApi.models().find(m => m.name === name);
if (modelInfo.name === "CLIP") {
const tokenizerUrl = new URL("./tokenizer.json", modelInfo.url).href;
this.model = new CLIPTransformerModel(modelInfo.url, tokenizerUrl)
this.setEmbeddingModel(modelInfo.id);
} else {
throw new Error("Unknown model: " + name);
}
},
onBadgeClick() {
this.$store.commit("setEmbedding", null);
this.$store.commit("setEmbeddingDoc", null);
}
}
}
</script>
<style>
.overlay-badge {
position: absolute;
z-index: 1;
left: 0.375rem;
top: 8px;
line-height: 1.1rem;
overflow: hidden;
max-width: 200px;
text-overflow: ellipsis;
}
.input-prepend {
max-width: 100px;
}
.theme-black .ml-append {
filter: brightness(0.95) !important;
}
</style>

View File

@@ -1,42 +1,46 @@
<template> <template>
<div class="featured-line" v-html="featuredLineHtml"></div> <div class="featured-line" v-html="featuredLineHtml"></div>
</template> </template>
<script> <script>
import {humanDate, humanFileSize} from "@/util"; import {humanDate, humanFileSize} from "@/util";
function scopedEval(context, expr) { function scopedEval(context, expr) {
const evaluator = Function.apply(null, [...Object.keys(context), "expr", "return eval(expr)"]); const evaluator = Function.apply(null, [...Object.keys(context), "expr", "return eval(expr)"]);
return evaluator.apply(null, [...Object.values(context), expr]); return evaluator.apply(null, [...Object.values(context), expr]);
} }
export default { export default {
name: "FeaturedFieldsLine", name: "FeaturedFieldsLine",
props: ["doc"], props: ["doc"],
computed: { computed: {
featuredLineHtml() { featuredLineHtml() {
const scope = {doc: this.doc._source, humanDate: humanDate, humanFileSize: humanFileSize}; if (this.$store.getters.optFeaturedFields === undefined) {
return "";
}
return this.$store.getters.optFeaturedFields const scope = {doc: this.doc._source, humanDate: humanDate, humanFileSize: humanFileSize};
.replaceAll(/\$\{([^}]*)}/g, (match, g1) => {
return scopedEval(scope, g1); return this.$store.getters.optFeaturedFields
}); .replaceAll(/\$\{([^}]*)}/g, (match, g1) => {
return scopedEval(scope, g1);
});
}
} }
}
} }
</script> </script>
<style scoped> <style scoped>
.featured-line { .featured-line {
font-size: 90%; font-size: 90%;
font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;
color: #424242; color: #424242;
padding-left: 2px; padding-left: 2px;
} }
.theme-black .featured-line { .theme-black .featured-line {
color: #bebebe; color: #bebebe;
} }
</style> </style>

View File

@@ -71,7 +71,7 @@ export default {
const doc = this.doc; const doc = this.doc;
const props = doc._props; const props = doc._props;
if (props.isGif && this.hover) { if (props.isGif && this.hover) {
return `f/${doc._id}`; return `f/${doc._source.index}/${doc._id}`;
} }
return (this.currentThumbnailNum === 0) return (this.currentThumbnailNum === 0)
? `t/${doc._source.index}/${doc._id}` ? `t/${doc._source.index}/${doc._id}`

View File

@@ -1,42 +1,46 @@
<template> <template>
<div v-if="isMobile"> <div v-if="isMobile">
<b-form-select <b-form-select
:value="selectedIndicesIds" :value="selectedIndicesIds"
@change="onSelect($event)" @change="onSelect($event)"
:options="indices" multiple :select-size="6" text-field="name" :options="indices" multiple :select-size="6" text-field="name"
value-field="id"></b-form-select> value-field="id"></b-form-select>
</div> </div>
<div v-else> <div v-else>
<div class="d-flex justify-content-between align-content-center"> <div class="d-flex justify-content-between align-content-center">
<span> <span>
{{ selectedIndices.length }} {{ selectedIndices.length }}
{{ selectedIndices.length === 1 ? $t("indexPicker.selectedIndex") : $t("indexPicker.selectedIndices") }} {{ selectedIndices.length === 1 ? $t("indexPicker.selectedIndex") : $t("indexPicker.selectedIndices") }}
</span> </span>
<div> <div>
<b-button variant="link" @click="selectAll()"> {{ $t("indexPicker.selectAll") }}</b-button> <b-button variant="link" @click="selectAll()"> {{ $t("indexPicker.selectAll") }}</b-button>
<b-button variant="link" @click="selectNone()"> {{ $t("indexPicker.selectNone") }}</b-button> <b-button variant="link" @click="selectNone()"> {{ $t("indexPicker.selectNone") }}</b-button>
</div> </div>
</div>
<b-list-group id="index-picker-desktop" class="unselectable">
<b-list-group-item
v-for="idx in indices"
@click="toggleIndex(idx, $event)"
@click.shift="shiftClick(idx, $event)"
class="d-flex justify-content-between align-items-center list-group-item-action pointer"
:class="{active: lastClickIndex === idx}"
>
<div class="d-flex">
<b-checkbox style="pointer-events: none" :checked="isSelected(idx)"></b-checkbox>
{{ idx.name }}
<span class="text-muted timestamp-text ml-2">{{ formatIdxDate(idx.timestamp) }}</span>
</div> </div>
<b-badge class="version-badge">v{{ idx.version }}</b-badge>
</b-list-group-item> <b-list-group id="index-picker-desktop" class="unselectable">
</b-list-group> <b-list-group-item
</div> v-for="idx in indices"
@click="toggleIndex(idx, $event)"
@click.shift="shiftClick(idx, $event)"
class="d-flex justify-content-between align-items-center list-group-item-action pointer"
:class="{active: lastClickIndex === idx}"
>
<div class="d-flex">
<b-checkbox style="pointer-events: none" :checked="isSelected(idx)"></b-checkbox>
{{ idx.name }}
<div style="vertical-align: center; margin-left: 5px">
<MLIcon small style="top: -1px; position: relative"></MLIcon>
</div>
<span class="text-muted timestamp-text ml-2"
style="top: 1px; position: relative">{{ formatIdxDate(idx.timestamp) }}</span>
</div>
<b-badge class="version-badge">v{{ idx.version }}</b-badge>
</b-list-group-item>
</b-list-group>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -44,148 +48,150 @@ import SmallBadge from "./SmallBadge.vue"
import {mapActions, mapGetters} from "vuex"; import {mapActions, mapGetters} from "vuex";
import Vue from "vue"; import Vue from "vue";
import {format} from "date-fns"; import {format} from "date-fns";
import MLIcon from "@/components/icons/MlIcon.vue";
export default Vue.extend({ export default Vue.extend({
components: { components: {
SmallBadge MLIcon,
}, SmallBadge
data() {
return {
loading: true,
lastClickIndex: null
}
},
computed: {
...mapGetters([
"indices", "selectedIndices"
]),
selectedIndicesIds() {
return this.selectedIndices.map(idx => idx.id)
}, },
isMobile() { data() {
return window.innerWidth <= 650; return {
} loading: true,
}, lastClickIndex: null
methods: {
...mapActions({
setSelectedIndices: "setSelectedIndices"
}),
shiftClick(index, e) {
if (this.lastClickIndex === null) {
return;
}
const select = this.isSelected(this.lastClickIndex);
let leftBoundary = this.indices.indexOf(this.lastClickIndex);
let rightBoundary = this.indices.indexOf(index);
if (rightBoundary < leftBoundary) {
let tmp = leftBoundary;
leftBoundary = rightBoundary;
rightBoundary = tmp;
}
for (let i = leftBoundary; i <= rightBoundary; i++) {
if (select) {
if (!this.isSelected(this.indices[i])) {
this.setSelectedIndices([this.indices[i], ...this.selectedIndices]);
}
} else {
this.setSelectedIndices(this.selectedIndices.filter(idx => idx !== this.indices[i]));
} }
}
}, },
selectAll() { computed: {
this.setSelectedIndices(this.indices); ...mapGetters([
"indices", "selectedIndices"
]),
selectedIndicesIds() {
return this.selectedIndices.map(idx => idx.id)
},
isMobile() {
return window.innerWidth <= 650;
}
}, },
selectNone() { methods: {
this.setSelectedIndices([]); ...mapActions({
}, setSelectedIndices: "setSelectedIndices"
onSelect(value) { }),
this.setSelectedIndices(this.indices.filter(idx => value.includes(idx.id))); shiftClick(index, e) {
}, if (this.lastClickIndex === null) {
formatIdxDate(timestamp: number): string { return;
return format(new Date(timestamp * 1000), "yyyy-MM-dd"); }
},
toggleIndex(index, e) {
if (e.shiftKey) {
return;
}
this.lastClickIndex = index; const select = this.isSelected(this.lastClickIndex);
if (this.isSelected(index)) {
this.setSelectedIndices(this.selectedIndices.filter(idx => idx.id != index.id)); let leftBoundary = this.indices.indexOf(this.lastClickIndex);
} else { let rightBoundary = this.indices.indexOf(index);
this.setSelectedIndices([index, ...this.selectedIndices]);
} if (rightBoundary < leftBoundary) {
let tmp = leftBoundary;
leftBoundary = rightBoundary;
rightBoundary = tmp;
}
for (let i = leftBoundary; i <= rightBoundary; i++) {
if (select) {
if (!this.isSelected(this.indices[i])) {
this.setSelectedIndices([this.indices[i], ...this.selectedIndices]);
}
} else {
this.setSelectedIndices(this.selectedIndices.filter(idx => idx !== this.indices[i]));
}
}
},
selectAll() {
this.setSelectedIndices(this.indices);
},
selectNone() {
this.setSelectedIndices([]);
},
onSelect(value) {
this.setSelectedIndices(this.indices.filter(idx => value.includes(idx.id)));
},
formatIdxDate(timestamp: number): string {
return format(new Date(timestamp * 1000), "yyyy-MM-dd");
},
toggleIndex(index, e) {
if (e.shiftKey) {
return;
}
this.lastClickIndex = index;
if (this.isSelected(index)) {
this.setSelectedIndices(this.selectedIndices.filter(idx => idx.id != index.id));
} else {
this.setSelectedIndices([index, ...this.selectedIndices]);
}
},
isSelected(index) {
return this.selectedIndices.find(idx => idx.id == index.id) != null;
}
}, },
isSelected(index) {
return this.selectedIndices.find(idx => idx.id == index.id) != null;
}
},
}) })
</script> </script>
<style scoped> <style scoped>
.timestamp-text { .timestamp-text {
line-height: 24px; line-height: 24px;
font-size: 80%; font-size: 80%;
} }
.theme-black .version-badge { .theme-black .version-badge {
color: #eee !important; color: #eee !important;
background: none; background: none;
} }
.version-badge { .version-badge {
color: #222 !important; color: #222 !important;
background: none; background: none;
} }
.list-group-item { .list-group-item {
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
} }
#index-picker-desktop { #index-picker-desktop {
overflow-y: auto; overflow-y: auto;
max-height: 132px; max-height: 132px;
} }
.btn-link:focus { .btn-link:focus {
box-shadow: none; box-shadow: none;
} }
.unselectable { .unselectable {
user-select: none; user-select: none;
-ms-user-select: none; -ms-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
} }
.list-group-item.active { .list-group-item.active {
z-index: 2; z-index: 2;
background-color: inherit; background-color: inherit;
color: inherit; color: inherit;
} }
.theme-black .list-group-item { .theme-black .list-group-item {
border: 1px solid rgba(255,255,255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
} }
.theme-black .list-group-item:first-child { .theme-black .list-group-item:first-child {
border: 1px solid rgba(255,255,255, 0.05); border: 1px solid rgba(255, 255, 255, 0.05);
} }
.theme-black .list-group-item.active { .theme-black .list-group-item.active {
z-index: 2; z-index: 2;
background-color: inherit; background-color: inherit;
color: inherit; color: inherit;
border: 1px solid rgba(255,255,255, 0.3); border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 0; border-radius: 0;
} }
.theme-black .list-group { .theme-black .list-group {
border-radius: 0; border-radius: 0;
} }
</style> </style>

View File

@@ -1,94 +1,167 @@
<template> <template>
<Preloader v-if="loading"></Preloader> <Preloader v-if="loading"></Preloader>
<div v-else-if="content" class="content-div" v-html="content"></div> <div v-else-if="content">
<b-form inline class="my-2" v-if="ModelsRepo.getOptions().length > 0">
<b-checkbox class="ml-auto mr-2" :checked="optAutoAnalyze"
@input="setOptAutoAnalyze($event); $store.dispatch('updateConfiguration')">
{{ $t("ml.auto") }}
</b-checkbox>
<b-button :disabled="mlPredictionsLoading || mlLoading" @click="mlAnalyze" variant="primary"
>{{ $t("ml.analyzeText") }}
</b-button>
<b-select :disabled="mlPredictionsLoading || mlLoading" class="ml-2" v-model="nerModel">
<b-select-option :value="opt.value" v-for="opt of ModelsRepo.getOptions()">{{ opt.text }}
</b-select-option>
</b-select>
</b-form>
<b-progress v-if="mlLoading" variant="warning" show-progress :max="1" class="mb-3"
>
<b-progress-bar :value="modelLoadingProgress">
<strong>{{ ((modelLoadingProgress * modelSize) / (1024 * 1024)).toFixed(1) }}MB / {{
(modelSize / (1024 * 1024)).toFixed(1)
}}MB</strong>
</b-progress-bar>
</b-progress>
<b-progress v-if="mlPredictionsLoading" variant="primary" :value="modelPredictionProgress"
:max="content.length" class="mb-3"></b-progress>
<AnalyzedContentSpansContainer v-if="analyzedContentSpans.length > 0"
:spans="analyzedContentSpans" :text="rawContent"></AnalyzedContentSpansContainer>
<div v-else class="content-div" v-html="content"></div>
</div>
</template> </template>
<script> <script>
import Sist2Api from "@/Sist2Api"; import Sist2Api from "@/Sist2Api";
import Preloader from "@/components/Preloader"; import Preloader from "@/components/Preloader";
import Sist2Query from "@/Sist2Query"; import Sist2Query from "@/Sist2ElasticsearchQuery";
import store from "@/store"; import store from "@/store";
import BertNerModel from "@/ml/BertNerModel";
import AnalyzedContentSpansContainer from "@/components/AnalyzedContentSpanContainer.vue";
import ModelsRepo from "@/ml/modelsRepo";
import {mapGetters, mapMutations} from "vuex";
export default { export default {
name: "LazyContentDiv", name: "LazyContentDiv",
components: {Preloader}, components: {AnalyzedContentSpansContainer, Preloader},
props: ["docId"], props: ["docId"],
data() { data() {
return { return {
content: "", ModelsRepo,
loading: true content: "",
rawContent: "",
loading: true,
modelLoadingProgress: 0,
modelPredictionProgress: 0,
mlPredictionsLoading: false,
mlLoading: false,
nerModel: null,
analyzedContentSpans: []
}
},
mounted() {
if (this.$store.getters.optMlDefaultModel) {
this.nerModel = this.$store.getters.optMlDefaultModel
} else {
this.nerModel = ModelsRepo.getDefaultModel();
}
Sist2Api
.getDocument(this.docId, this.$store.state.optHighlight, this.$store.state.fuzzy)
.then(doc => {
this.loading = false;
if (doc) {
this.content = this.getContent(doc)
}
if (this.optAutoAnalyze) {
this.mlAnalyze();
}
});
},
computed: {
...mapGetters(["optAutoAnalyze"]),
modelSize() {
const modelData = ModelsRepo.data[this.nerModel];
if (!modelData) {
return 0;
}
return modelData.size;
}
},
methods: {
...mapMutations(["setOptAutoAnalyze"]),
getContent(doc) {
this.rawContent = doc._source.content;
if (!doc.highlight) {
return doc._source.content;
}
if (doc.highlight["content.nGram"]) {
return doc.highlight["content.nGram"][0];
}
if (doc.highlight.content) {
return doc.highlight.content[0];
}
},
async getMlModel() {
if (this.$store.getters.nerModel.name !== this.nerModel) {
this.mlLoading = true;
this.modelLoadingProgress = 0;
const modelInfo = ModelsRepo.data[this.nerModel];
const model = new BertNerModel(
modelInfo.vocabUrl,
modelInfo.modelUrl,
modelInfo.id2label,
)
await model.init(progress => this.modelLoadingProgress = progress);
this.$store.commit("setNerModel", {model, name: this.nerModel});
this.mlLoading = false;
return model
}
return this.$store.getters.nerModel.model;
},
async mlAnalyze() {
if (!this.content) {
return;
}
const modelInfo = ModelsRepo.data[this.nerModel];
if (modelInfo === undefined) {
return;
}
this.$store.commit("setOptMlDefaultModel", this.nerModel);
await this.$store.dispatch("updateConfiguration");
const model = await this.getMlModel();
this.analyzedContentSpans = [];
this.mlPredictionsLoading = true;
await model.predict(this.rawContent, results => {
results.forEach(result => result.label = modelInfo.humanLabels[result.label]);
this.analyzedContentSpans.push(...results);
this.modelPredictionProgress = results[results.length - 1].wordIndex;
});
this.mlPredictionsLoading = false;
}
} }
},
mounted() {
const query = Sist2Query.searchQuery();
if (this.$store.state.optHighlight) {
const fields = this.$store.state.fuzzy
? {"content.nGram": {}}
: {content: {}};
query.highlight = {
pre_tags: ["<mark>"],
post_tags: ["</mark>"],
number_of_fragments: 0,
fields,
};
if (!store.state.sist2Info.esVersionLegacy) {
query.highlight.max_analyzed_offset = 999_999;
}
}
if ("function_score" in query.query) {
query.query = query.query.function_score.query;
}
if (!("must" in query.query.bool)) {
query.query.bool.must = [];
} else if (!Array.isArray(query.query.bool.must)) {
query.query.bool.must = [query.query.bool.must];
}
query.query.bool.must.push({match: {_id: this.docId}});
delete query["sort"];
delete query["aggs"];
delete query["search_after"];
delete query.query["function_score"];
query._source = {
includes: ["content", "name", "path", "extension"]
}
query.size = 1;
Sist2Api.esQuery(query).then(resp => {
this.loading = false;
if (resp.hits.hits.length === 1) {
this.content = this.getContent(resp.hits.hits[0]);
} else {
console.log("FIXME: could not get content")
console.log(resp)
}
});
},
methods: {
getContent(doc) {
if (!doc.highlight) {
return doc._source.content;
}
if (doc.highlight["content.nGram"]) {
return doc.highlight["content.nGram"][0];
}
if (doc.highlight.content) {
return doc.highlight.content[0];
}
}
}
} }
</script> </script>
<style scoped> <style>
.progress-bar {
transition: none;
}
</style> </style>

View File

@@ -9,7 +9,7 @@ import InspireTreeDOM from "inspire-tree-dom";
import "inspire-tree-dom/dist/inspire-tree-light.min.css"; import "inspire-tree-dom/dist/inspire-tree-light.min.css";
import {getSelectedTreeNodes, getTreeNodeAttributes} from "@/util"; import {getSelectedTreeNodes, getTreeNodeAttributes} from "@/util";
import Sist2Api from "@/Sist2Api"; import Sist2Api from "@/Sist2Api";
import Sist2Query from "@/Sist2Query"; import Sist2Query from "@/Sist2ElasticsearchQuery";
export default { export default {
name: "MimePicker", name: "MimePicker",

View File

@@ -10,7 +10,7 @@
<span class="badge badge-pill version" v-if="$store && $store.state.sist2Info"> <span class="badge badge-pill version" v-if="$store && $store.state.sist2Info">
v{{ sist2Version() }}<span v-if="isDebug()">-dbg</span><span v-if="isLegacy() && !hideLegacy()">-<a v{{ sist2Version() }}<span v-if="isDebug()">-dbg</span><span v-if="isLegacy() && !hideLegacy()">-<a
href="https://github.com/simon987/sist2/blob/master/docs/USAGE.md#elasticsearch" href="https://github.com/simon987/sist2/blob/master/docs/USAGE.md#elasticsearch"
target="_blank">legacyES</a></span> target="_blank">legacyES</a></span><span v-if="$store.state.uiSqliteMode">-SQLite</span>
</span> </span>
<span v-if="$store && $store.state.sist2Info" class="tagline" v-html="tagline()"></span> <span v-if="$store && $store.state.sist2Info" class="tagline" v-html="tagline()"></span>

View File

@@ -1,40 +1,41 @@
<template> <template>
<div> <div>
<div class="input-group" style="margin-bottom: 0.5em; margin-top: 1em"> <div class="input-group" style="margin-bottom: 0.5em; margin-top: 1em">
<div class="input-group-prepend"> <div class="input-group-prepend">
<b-button variant="outline-secondary" @click="$refs['path-modal'].show()"> <b-button variant="outline-secondary" @click="$refs['path-modal'].show()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="20px"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="20px">
<path <path
fill="currentColor" fill="currentColor"
d="M288 224h224a32 32 0 0 0 32-32V64a32 32 0 0 0-32-32H400L368 0h-80a32 32 0 0 0-32 32v64H64V8a8 8 0 0 0-8-8H40a8 8 0 0 0-8 8v392a16 16 0 0 0 16 16h208v64a32 32 0 0 0 32 32h224a32 32 0 0 0 32-32V352a32 32 0 0 0-32-32H400l-32-32h-80a32 32 0 0 0-32 32v64H64V128h192v64a32 32 0 0 0 32 32zm0 96h66.74l32 32H512v128H288zm0-288h66.74l32 32H512v128H288z" d="M288 224h224a32 32 0 0 0 32-32V64a32 32 0 0 0-32-32H400L368 0h-80a32 32 0 0 0-32 32v64H64V8a8 8 0 0 0-8-8H40a8 8 0 0 0-8 8v392a16 16 0 0 0 16 16h208v64a32 32 0 0 0 32 32h224a32 32 0 0 0 32-32V352a32 32 0 0 0-32-32H400l-32-32h-80a32 32 0 0 0-32 32v64H64V128h192v64a32 32 0 0 0 32 32zm0 96h66.74l32 32H512v128H288zm0-288h66.74l32 32H512v128H288z"
/> />
</svg> </svg>
</b-button> </b-button>
</div> </div>
<VueSimpleSuggest
class="form-control-fix-flex"
@input="setPathText"
:value="getPathText"
:list="suggestPath"
:max-suggestions="0"
:placeholder="$t('pathBar.placeholder')"
:debounce="200"
>
<!-- Suggestion item template-->
<div slot="suggestion-item" slot-scope="{ suggestion, query }">
<div class="suggestion-line" :title="suggestion">
<strong>{{ query }}</strong>{{ getSuggestionWithoutQueryPrefix(suggestion, query) }}
</div>
</div>
</VueSimpleSuggest>
<VueSimpleSuggest
class="form-control-fix-flex"
@input="setPathText"
:value="getPathText"
:list="suggestPath"
:max-suggestions="0"
:placeholder="$t('pathBar.placeholder')"
>
<!-- Suggestion item template-->
<div slot="suggestion-item" slot-scope="{ suggestion, query }">
<div class="suggestion-line" :title="suggestion">
<strong>{{ query }}</strong>{{ getSuggestionWithoutQueryPrefix(suggestion, query) }}
</div>
</div> </div>
</VueSimpleSuggest>
<b-modal ref="path-modal" :title="$t('pathBar.modalTitle')" size="lg" :hide-footer="true" static>
<div id="pathTree"></div>
</b-modal>
</div> </div>
<b-modal ref="path-modal" :title="$t('pathBar.modalTitle')" size="lg" :hide-footer="true" static>
<div id="pathTree"></div>
</b-modal>
</div>
</template> </template>
<script> <script>
@@ -48,198 +49,153 @@ import VueSimpleSuggest from 'vue-simple-suggest'
import 'vue-simple-suggest/dist/styles.css' // Optional CSS import 'vue-simple-suggest/dist/styles.css' // Optional CSS
export default { export default {
name: "PathTree", name: "PathTree",
components: { components: {
VueSimpleSuggest VueSimpleSuggest
},
data() {
return {
mimeTree: null,
pathItems: [],
tmpPath: ""
}
},
computed: {
...mapGetters(["getPathText"])
},
mounted() {
this.$store.subscribe((mutation) => {
// Wait until indices are loaded to get the root paths
if (mutation.type === "setIndices") {
let pathTree = new InspireTree({
data: (node, resolve, reject) => {
return this.getNextDepth(node);
},
sort: "text"
});
this.$store.state.indices.forEach(idx => {
pathTree.addNode({
id: "/" + idx.id,
values: ["/" + idx.id],
text: `/[${idx.name}]`,
index: idx.id,
depth: 0,
children: true
})
});
new InspireTreeDOM(pathTree, {
target: "#pathTree"
});
pathTree.on("node.click", this.handleTreeClick);
pathTree.expand();
}
});
},
methods: {
...mapMutations(["setPathText"]),
getSuggestionWithoutQueryPrefix(suggestion, query) {
return suggestion.slice(query.length)
}, },
async getPathChoices() { data() {
return new Promise(getPaths => { return {
const q = { mimeTree: null,
suggest: { pathItems: [],
path: { tmpPath: ""
prefix: this.getPathText,
completion: {
field: "suggest-path",
skip_duplicates: true,
size: 10000
}
}
}
};
Sist2Api.esQuery(q)
.then(resp => getPaths(resp["suggest"]["path"][0]["options"].map(opt => opt["_source"]["path"])));
})
},
async suggestPath(term) {
if (!this.$store.state.optSuggestPath) {
return []
}
term = term.toLowerCase();
const choices = await this.getPathChoices();
let matches = [];
for (let i = 0; i < choices.length; i++) {
if (~choices[i].toLowerCase().indexOf(term)) {
matches.push(choices[i]);
} }
}
return matches.sort((a, b) => a.length - b.length);
}, },
getNextDepth(node) { computed: {
const q = { ...mapGetters(["getPathText"])
query: { },
bool: { mounted() {
filter: [ this.$store.subscribe((mutation) => {
{term: {index: node.index}}, // Wait until indices are loaded to get the root paths
{range: {_depth: {gte: node.depth + 1, lte: node.depth + 3}}}, if (mutation.type === "setIndices") {
] let pathTree = new InspireTree({
} data: (node, resolve, reject) => {
}, return this.getNextDepth(node);
aggs: { },
paths: { sort: "text"
terms: { });
field: "path",
size: 10000 this.$store.state.indices.forEach(idx => {
pathTree.addNode({
id: "/" + idx.id,
values: ["/" + idx.id],
text: `/[${idx.name}]`,
index: idx.id,
depth: 0,
children: true
})
});
new InspireTreeDOM(pathTree, {
target: "#pathTree"
});
pathTree.on("node.click", this.handleTreeClick);
pathTree.expand();
} }
} });
},
methods: {
...mapMutations(["setPathText"]),
getSuggestionWithoutQueryPrefix(suggestion, query) {
return suggestion.slice(query.length)
}, },
size: 0 async getPathChoices() {
}; return new Promise(getPaths => {
Sist2Api.getPathSuggestions(this.getPathText).then(getPaths);
});
},
async suggestPath(term) {
if (!this.$store.state.optSuggestPath) {
return []
}
if (node.depth > 0) { term = term.toLowerCase();
q.query.bool.must = {
prefix: {
path: node.id,
}
};
}
return Sist2Api.esQuery(q).then(resp => { const choices = await this.getPathChoices();
const buckets = resp["aggregations"]["paths"]["buckets"];
if (!buckets) {
return false;
}
const paths = []; let matches = [];
for (let i = 0; i < choices.length; i++) {
if (~choices[i].toLowerCase().indexOf(term)) {
matches.push(choices[i]);
}
}
return matches.sort((a, b) => a.length - b.length);
},
getNextDepth(node) {
return Sist2Api
.searchPaths(node.index, node.depth + 1, node.depth + 3, node.depth > 0 ? node.id : null)
.then(buckets => {
const paths = [];
return buckets return buckets
.filter(bucket => bucket.key.length > node.id.length || node.id.startsWith("/")) .filter(bucket => bucket.path.length > node.id.length || node.id.startsWith("/"))
.sort((a, b) => a.key > b.key) .sort((a, b) => a.path > b.path ? 1 : -1)
.map(bucket => { .map(bucket => {
if (paths.some(n => bucket.path.startsWith(n))) {
return null;
}
if (paths.some(n => bucket.key.startsWith(n))) { const name = node.id.startsWith("/") ? bucket.path : bucket.path.slice(node.id.length + 1);
return null;
}
const name = node.id.startsWith("/") ? bucket.key : bucket.key.slice(node.id.length + 1); paths.push(bucket.path);
paths.push(bucket.key); return {
id: bucket.path,
text: `${name}/ (${bucket.count})`,
depth: node.depth + 1,
index: node.index,
values: [bucket.path],
children: true,
}
})
.filter(bucket => bucket !== null);
});
},
handleTreeClick(e, node, handler) {
if (node.depth !== 0) {
this.setPathText(node.id);
this.$refs['path-modal'].hide()
return { this.$emit("search");
id: bucket.key, }
text: `${name}/ (${bucket.doc_count})`,
depth: node.depth + 1, handler();
index: node.index, },
values: [bucket.key],
children: true,
}
}).filter(x => x !== null)
});
}, },
handleTreeClick(e, node, handler) {
if (node.depth !== 0) {
this.setPathText(node.id);
this.$refs['path-modal'].hide()
this.$emit("search");
}
handler();
},
},
} }
</script> </script>
<style scoped> <style scoped>
#mimeTree { #mimeTree {
max-height: 350px; max-height: 350px;
overflow: auto; overflow: auto;
} }
.form-control-fix-flex { .form-control-fix-flex {
flex: 1 1 auto; flex: 1 1 auto;
width: 1%; width: 1%;
min-width: 0; min-width: 0;
margin-bottom: 0; margin-bottom: 0;
} }
.suggestion-line { .suggestion-line {
max-width: 100%; max-width: 100%;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
line-height: 1.1; line-height: 1.1;
} }
</style> </style>
<style> <style>
.suggestions { .suggestions {
max-height: 250px; max-height: 250px;
overflow-y: auto; overflow-y: auto;
} }
.theme-black .suggestions { .theme-black .suggestions {
color: black color: black
} }
</style> </style>

View File

@@ -1,40 +1,46 @@
<template> <template>
<b-card v-if="lastResultsLoaded" id="results"> <b-card v-if="lastResultsLoaded" id="results">
<span>{{ hitCount }} {{ hitCount === 1 ? $t("hit") : $t("hits") }}</span> <span>{{ hitCount }} {{ hitCount === 1 ? $t("hit") : $t("hits") }}</span>
<div style="float: right"> <div style="float: right">
<b-button v-b-toggle.collapse-1 variant="primary" class="not-mobile" @click="onToggle()">{{ <b-button v-b-toggle.collapse-1 variant="primary" class="not-mobile" @click="onToggle()">{{
$t("details") $t("details")
}} }}
</b-button> </b-button>
<template v-if="hitCount !== 0"> <template v-if="hitCount !== 0">
<SortSelect class="ml-2"></SortSelect> <SortSelect class="ml-2"></SortSelect>
<DisplayModeToggle class="ml-2"></DisplayModeToggle> <DisplayModeToggle class="ml-2"></DisplayModeToggle>
</template> </template>
</div> </div>
<b-collapse id="collapse-1" class="pt-2" style="clear:both;"> <b-collapse id="collapse-1" class="pt-2" style="clear:both;">
<b-card> <b-card>
<b-table :items="tableItems" small borderless bordered thead-class="hidden" class="mb-0"></b-table> <b-table :items="tableItems" small borderless thead-class="hidden" class="mb-0"></b-table>
<br/> <template v-if="!$store.state.uiSqliteMode">
<h4>
{{$t("mimeTypes")}} <br/>
<b-button size="sm" variant="primary" class="float-right" @click="onCopyClick"><ClipboardIcon/></b-button> <h4>
</h4> {{ $t("mimeTypes") }}
<Preloader v-if="$store.state.uiDetailsMimeAgg == null"></Preloader> <b-button size="sm" variant="primary" class="float-right" @click="onCopyClick">
<b-table <ClipboardIcon/>
v-else </b-button>
sort-by="doc_count" </h4>
:sort-desc="true" <Preloader v-if="$store.state.uiDetailsMimeAgg == null"></Preloader>
thead-class="hidden" <b-table
:items="$store.state.uiDetailsMimeAgg" small bordered class="mb-0" v-else
></b-table> sort-by="doc_count"
</b-card> :sort-desc="true"
</b-collapse> thead-class="hidden"
</b-card> bordered
:items="$store.state.uiDetailsMimeAgg" small class="mb-0"
></b-table>
</template>
</b-card>
</b-collapse>
</b-card>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -44,91 +50,96 @@ import {humanFileSize} from "@/util";
import DisplayModeToggle from "@/components/DisplayModeToggle.vue"; import DisplayModeToggle from "@/components/DisplayModeToggle.vue";
import SortSelect from "@/components/SortSelect.vue"; import SortSelect from "@/components/SortSelect.vue";
import Preloader from "@/components/Preloader.vue"; import Preloader from "@/components/Preloader.vue";
import Sist2Query from "@/Sist2Query"; import Sist2Query from "@/Sist2ElasticsearchQuery";
import ClipboardIcon from "@/components/icons/ClipboardIcon.vue"; import ClipboardIcon from "@/components/icons/ClipboardIcon.vue";
export default Vue.extend({ export default Vue.extend({
name: "ResultsCard", name: "ResultsCard",
components: {ClipboardIcon, Preloader, SortSelect, DisplayModeToggle}, components: {ClipboardIcon, Preloader, SortSelect, DisplayModeToggle},
created() { created() {
},
computed: {
lastResultsLoaded() {
return this.$store.state.lastQueryResults != null;
}, },
hitCount() { computed: {
return (this.$store.state.lastQueryResults as EsResult).aggregations.total_count.value; lastResultsLoaded() {
return this.$store.state.lastQueryResults != null;
},
hitCount() {
return (this.$store.state.firstQueryResults as EsResult).aggregations.total_count.value;
},
tableItems() {
const items = [];
if (!this.$store.state.uiSqliteMode) {
items.push({key: this.$t("queryTime"), value: this.took()});
}
items.push({key: this.$t("totalSize"), value: this.totalSize()});
return items;
}
}, },
tableItems() { methods: {
const items = []; took() {
return (this.$store.state.lastQueryResults as EsResult).took + "ms";
},
totalSize() {
return humanFileSize((this.$store.state.firstQueryResults as EsResult).aggregations.total_size.value);
},
onToggle() {
const show = !document.getElementById("collapse-1").classList.contains("show");
this.$store.commit("setUiShowDetails", show);
if (this.$store.state.uiSqliteMode) {
return;
}
items.push({key: this.$t("queryTime"), value: this.took()}); if (show && this.$store.state.uiDetailsMimeAgg == null && !this.$store.state.optUpdateMimeMap) {
items.push({key: this.$t("totalSize"), value: this.totalSize()}); // Mime aggs are not updated automatically, update now
this.forceUpdateMimeAgg();
}
},
onCopyClick() {
let tsvString = "";
this.$store.state.uiDetailsMimeAgg.slice().sort((a, b) => b["doc_count"] - a["doc_count"]).forEach(row => {
tsvString += `${row["key"]}\t${row["doc_count"]}\n`;
});
return items; navigator.clipboard.writeText(tsvString);
}
}, this.$bvToast.toast(
methods: { this.$t("toast.copiedToClipboard"),
took() { {
return (this.$store.state.lastQueryResults as EsResult).took + "ms"; title: null,
noAutoHide: false,
toaster: "b-toaster-bottom-right",
headerClass: "hidden",
bodyClass: "toast-body-info",
});
},
forceUpdateMimeAgg() {
const query = Sist2Query.searchQuery();
Sist2Api.getMimeTypes(query).then(({buckets}) => {
this.$store.commit("setUiDetailsMimeAgg", buckets);
});
}
}, },
totalSize() {
return humanFileSize((this.$store.state.lastQueryResults as EsResult).aggregations.total_size.value);
},
onToggle() {
const show = !document.getElementById("collapse-1").classList.contains("show");
this.$store.commit("setUiShowDetails", show);
if (show && this.$store.state.uiDetailsMimeAgg == null && !this.$store.state.optUpdateMimeMap) {
// Mime aggs are not updated automatically, update now
this.forceUpdateMimeAgg();
}
},
onCopyClick() {
let tsvString = "";
this.$store.state.uiDetailsMimeAgg.slice().sort((a,b) => b["doc_count"] - a["doc_count"]).forEach(row => {
tsvString += `${row["key"]}\t${row["doc_count"]}\n`;
});
navigator.clipboard.writeText(tsvString);
this.$bvToast.toast(
this.$t("toast.copiedToClipboard"),
{
title: null,
noAutoHide: false,
toaster: "b-toaster-bottom-right",
headerClass: "hidden",
bodyClass: "toast-body-info",
});
},
forceUpdateMimeAgg() {
const query = Sist2Query.searchQuery();
Sist2Api.getMimeTypes(query).then(({buckets}) => {
this.$store.commit("setUiDetailsMimeAgg", buckets);
});
}
},
}); });
</script> </script>
<style> <style>
#results { #results {
margin-top: 1em; margin-top: 1em;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important; box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important;
border-radius: 0; border-radius: 0;
border: none; border: none;
} }
#results .card-body { #results .card-body {
padding: 0.7em 1.25em; padding: 0.7em 1.25em;
} }
.hidden { .hidden {
display: none; display: none;
} }
</style> </style>

View File

@@ -6,7 +6,7 @@
@input="setSearchText($event)"></b-form-input> @input="setSearchText($event)"></b-form-input>
<template #prepend> <template #prepend>
<b-input-group-text> <b-input-group-text v-if="!$store.state.uiSqliteMode">
<b-form-checkbox :checked="fuzzy" title="Toggle fuzzy searching" @change="setFuzzy($event)"> <b-form-checkbox :checked="fuzzy" title="Toggle fuzzy searching" @change="setFuzzy($event)">
{{ $t("searchBar.fuzzy") }} {{ $t("searchBar.fuzzy") }}
</b-form-checkbox> </b-form-checkbox>

View File

@@ -1,5 +1,5 @@
<template> <template>
<b-dropdown variant="primary"> <b-dropdown variant="primary" :disabled="$store.getters.embedding !== null">
<b-dropdown-item :class="{'dropdown-active': sort === 'score'}" @click="onSelect('score')">{{ <b-dropdown-item :class="{'dropdown-active': sort === 'score'}" @click="onSelect('score')">{{
$t("sort.relevance") $t("sort.relevance")
}} }}

View File

@@ -1,76 +1,78 @@
<template> <template>
<div @mouseenter="showAddButton = true" @mouseleave="showAddButton = false"> <div @mouseenter="showAddButton = true" @mouseleave="showAddButton = false">
<b-modal v-model="showModal" :title="$t('saveTagModalTitle')" hide-footer no-fade centered size="lg" static lazy> <b-modal v-model="showModal" :title="$t('saveTagModalTitle')" hide-footer no-fade centered size="lg" static
<b-row> lazy>
<b-col style="flex-grow: 2" sm> <b-row>
<VueSimpleSuggest <b-col style="flex-grow: 2" sm>
ref="suggest" <VueSimpleSuggest
:value="tagText" ref="suggest"
@select="setTagText($event)" :value="tagText"
@input="setTagText($event)" @select="setTagText($event)"
class="form-control-fix-flex" @input="setTagText($event)"
style="margin-top: 17px" class="form-control-fix-flex"
:list="suggestTag" style="margin-top: 17px"
:max-suggestions="0" :list="suggestTag"
:placeholder="$t('saveTagPlaceholder')" :max-suggestions="0"
> :placeholder="$t('saveTagPlaceholder')"
<!-- Suggestion item template--> >
<div slot="suggestion-item" slot-scope="{ suggestion, query}" <!-- Suggestion item template-->
> <div slot="suggestion-item" slot-scope="{ suggestion, query}"
<div class="suggestion-line"> >
<div class="suggestion-line">
<span <span
class="badge badge-suggestion" class="badge badge-suggestion"
:style="{background: getBg(suggestion), color: getFg(suggestion)}" :style="{background: getBg(suggestion), color: getFg(suggestion)}"
> >
<strong>{{ query }}</strong>{{ getSuggestionWithoutQueryPrefix(suggestion, query) }} <strong>{{ query }}</strong>{{ getSuggestionWithoutQueryPrefix(suggestion, query) }}
</span> </span>
</div> </div>
</div> </div>
</VueSimpleSuggest> </VueSimpleSuggest>
</b-col> </b-col>
<b-col class="mt-4"> <b-col class="mt-4">
<TwitterColorPicker v-model="color" triangle="hide" :width="252" class="mr-auto ml-auto"></TwitterColorPicker> <TwitterColorPicker v-model="color" triangle="hide" :width="252"
</b-col> class="mr-auto ml-auto"></TwitterColorPicker>
</b-row> </b-col>
</b-row>
<b-button variant="primary" style="float: right" class="mt-2" @click="saveTag()">{{ $t("confirm") }} <b-button variant="primary" style="float: right" class="mt-2" @click="saveTag()">{{ $t("confirm") }}
</b-button> </b-button>
</b-modal> </b-modal>
<template v-for="tag in hit._tags"> <template v-for="tag in hit._tags">
<!-- User tag--> <!-- User tag-->
<div v-if="tag.userTag" :key="tag.rawText" style="display: inline-block"> <div v-if="tag.userTag" :key="tag.rawText" style="display: inline-block">
<span <span
:id="hit._id+tag.rawText" :id="hit._id+tag.rawText"
:title="tag.text" :title="tag.text"
tabindex="-1" tabindex="-1"
class="badge pointer" class="badge pointer"
:style="badgeStyle(tag)" :class="badgeClass(tag)" :style="badgeStyle(tag)" :class="badgeClass(tag)"
@click.right="onTagRightClick(tag, $event)" @click.right="onTagRightClick(tag, $event)"
>{{ tag.text.split(".").pop() }}</span> >{{ tag.text.split(".").pop() }}</span>
<b-popover :target="hit._id+tag.rawText" triggers="focus blur" placement="top"> <b-popover :target="hit._id+tag.rawText" triggers="focus blur" placement="top">
<b-button variant="danger" @click="onTagDeleteClick(tag, $event)">{{ $t("deleteTag") }}</b-button> <b-button variant="danger" @click="onTagDeleteClick(tag, $event)">{{ $t("deleteTag") }}</b-button>
</b-popover> </b-popover>
</div> </div>
<span <span
v-else :key="tag.text" v-else :key="tag.text"
class="badge" class="badge"
:style="badgeStyle(tag)" :class="badgeClass(tag)" :style="badgeStyle(tag)" :class="badgeClass(tag)"
>{{ tag.text.split(".").pop() }}</span> >{{ tag.text.split(".").pop() }}</span>
</template> </template>
<!-- Add button --> <!-- Add button -->
<small v-if="showAddButton" class="badge add-tag-button" @click="tagAdd()">{{$t("addTag")}}</small> <small v-if="showAddButton" class="badge add-tag-button" @click="tagAdd()">{{ $t("addTag") }}</small>
<!-- Size tag--> <!-- Size tag-->
<small v-else class="text-muted badge-size" style="padding-left: 2px">{{ <small v-else class="text-muted badge-size" style="padding-left: 2px">{{
humanFileSize(hit._source.size) humanFileSize(hit._source.size)
}}</small> }}</small>
</div> </div>
</template> </template>
<script> <script>
@@ -81,170 +83,136 @@ import Sist2Api from "@/Sist2Api";
import VueSimpleSuggest from 'vue-simple-suggest' import VueSimpleSuggest from 'vue-simple-suggest'
export default Vue.extend({ export default Vue.extend({
components: { components: {
"TwitterColorPicker": Twitter, "TwitterColorPicker": Twitter,
VueSimpleSuggest VueSimpleSuggest
},
props: ["hit"],
data() {
return {
showAddButton: false,
showModal: false,
tagText: null,
color: {
hex: "#e0e0e0",
},
}
},
computed: {
tagHover() {
return this.$store.getters["uiTagHover"];
}
},
methods: {
humanFileSize: humanFileSize,
getSuggestionWithoutQueryPrefix(suggestion, query) {
return suggestion.id.slice(query.length, -8)
}, },
getBg(suggestion) { props: ["hit"],
return suggestion.id.slice(-7); data() {
},
getFg(suggestion) {
return lum(suggestion.id.slice(-7)) > 50 ? "#000" : "#fff";
},
setTagText(value) {
this.$refs.suggest.clearSuggestions();
if (typeof value === "string") {
this.tagText = {
id: value,
title: value
};
return;
}
this.color = {
hex: "#" + value.id.split("#")[1]
}
this.tagText = value;
},
badgeClass(tag) {
return `badge-${tag.style}`;
},
badgeStyle(tag) {
return {
background: tag.bg,
color: tag.fg,
};
},
onTagHover(tag) {
if (tag.userTag) {
this.$store.commit("setUiTagHover", tag);
}
},
onTagLeave() {
this.$store.commit("setUiTagHover", null);
},
onTagDeleteClick(tag, e) {
this.hit._tags = this.hit._tags.filter(t => t !== tag);
Sist2Api.deleteTag(tag.rawText, this.hit).then(() => {
//toast
this.$store.commit("busUpdateWallItems");
this.$store.commit("busUpdateTags");
});
},
tagAdd() {
this.showModal = true;
},
saveTag() {
if (this.tagText.id.includes("#")) {
this.$bvToast.toast(
this.$t("toast.invalidTag"),
{
title: this.$t("toast.invalidTagTitle"),
noAutoHide: true,
toaster: "b-toaster-bottom-right",
headerClass: "toast-header-error",
bodyClass: "toast-body-error",
});
return;
}
let tag = this.tagText.id + this.color.hex.replace("#", ".#");
const userTags = this.hit._tags.filter(t => t.userTag);
if (userTags.find(t => t.rawText === tag) != null) {
this.$bvToast.toast(
this.$t("toast.dupeTag"),
{
title: this.$t("toast.dupeTagTitle"),
noAutoHide: true,
toaster: "b-toaster-bottom-right",
headerClass: "toast-header-error",
bodyClass: "toast-body-error",
});
return;
}
this.hit._tags.push(Sist2Api.createUserTag(tag));
Sist2Api.saveTag(tag, this.hit).then(() => {
this.tagText = null;
this.showModal = false;
this.$store.commit("busUpdateWallItems");
this.$store.commit("busUpdateTags");
// TODO: toast
});
},
async suggestTag(term) {
term = term.toLowerCase();
const choices = await this.getTagChoices(term);
let matches = [];
for (let i = 0; i < choices.length; i++) {
if (~choices[i].toLowerCase().indexOf(term)) {
matches.push(choices[i]);
}
}
return matches.sort().map(match => {
return { return {
title: match.split(".").slice(0, -1).join("."), showAddButton: false,
id: match showModal: false,
tagText: null,
color: {
hex: "#e0e0e0",
},
} }
});
}, },
getTagChoices(prefix) { methods: {
return new Promise(getPaths => { humanFileSize: humanFileSize,
Sist2Api.esQuery({ getSuggestionWithoutQueryPrefix(suggestion, query) {
suggest: { return suggestion.id.slice(query.length, -8)
tag: { },
prefix: prefix, getBg(suggestion) {
completion: { return suggestion.id.slice(-7);
field: "suggest-tag", },
skip_duplicates: true, getFg(suggestion) {
size: 10000 return lum(suggestion.id.slice(-7)) > 50 ? "#000" : "#fff";
} },
setTagText(value) {
this.$refs.suggest.clearSuggestions();
if (typeof value === "string") {
this.tagText = {
id: value,
title: value
};
return;
} }
}
}).then(resp => { this.color = {
const result = []; hex: "#" + value.id.split("#")[1]
resp["suggest"]["tag"][0]["options"].map(opt => opt["_source"]["tag"]).forEach(tags => { }
tags.forEach(tag => {
const t = tag.slice(0, -8); this.tagText = value;
if (!result.find(x => x.slice(0, -8) === t)) { },
result.push(tag); badgeClass(tag) {
} return `badge-${tag.style}`;
},
badgeStyle(tag) {
return {
background: tag.bg,
color: tag.fg,
};
},
onTagDeleteClick(tag, e) {
this.hit._tags = this.hit._tags.filter(t => t !== tag);
Sist2Api.deleteTag(tag.rawText, this.hit).then(() => {
//toast
this.$store.commit("busUpdateWallItems");
this.$store.commit("busUpdateTags");
}); });
}); },
getPaths(result); tagAdd() {
}); this.showModal = true;
}); },
} saveTag() {
}, if (this.tagText.id.includes("#")) {
this.$bvToast.toast(
this.$t("toast.invalidTag"),
{
title: this.$t("toast.invalidTagTitle"),
noAutoHide: true,
toaster: "b-toaster-bottom-right",
headerClass: "toast-header-error",
bodyClass: "toast-body-error",
});
return;
}
let tag = this.tagText.id + this.color.hex.replace("#", ".#");
const userTags = this.hit._tags.filter(t => t.userTag);
if (userTags.find(t => t.rawText === tag) != null) {
this.$bvToast.toast(
this.$t("toast.dupeTag"),
{
title: this.$t("toast.dupeTagTitle"),
noAutoHide: true,
toaster: "b-toaster-bottom-right",
headerClass: "toast-header-error",
bodyClass: "toast-body-error",
});
return;
}
this.hit._tags.push(Sist2Api.createUserTag(tag));
Sist2Api.saveTag(tag, this.hit).then(() => {
this.tagText = null;
this.showModal = false;
this.$store.commit("busUpdateWallItems");
this.$store.commit("busUpdateTags");
// TODO: toast
});
},
async suggestTag(term) {
term = term.toLowerCase();
const choices = await this.getTagChoices(term);
let matches = [];
for (let i = 0; i < choices.length; i++) {
if (~choices[i].toLowerCase().indexOf(term)) {
matches.push(choices[i]);
}
}
return matches.sort().map(match => {
return {
title: match.split(".").slice(0, -1).join("."),
id: match
}
});
},
getTagChoices(prefix) {
return new Promise(getPaths => {
Sist2Api.getTagSuggestions(prefix)
.then(paths => getPaths(paths))
});
}
},
}); });
</script> </script>
@@ -252,87 +220,87 @@ export default Vue.extend({
.badge-video { .badge-video {
color: #FFFFFF; color: #FFFFFF;
background-color: #F27761; background-color: #F27761;
} }
.badge-image { .badge-image {
color: #FFFFFF; color: #FFFFFF;
background-color: #AA99C9; background-color: #AA99C9;
} }
.badge-audio { .badge-audio {
color: #FFFFFF; color: #FFFFFF;
background-color: #00ADEF; background-color: #00ADEF;
} }
.badge-user { .badge-user {
color: #212529; color: #212529;
background-color: #e0e0e0; background-color: #e0e0e0;
} }
.badge-user:hover, .add-tag-button:hover { .badge-user:hover, .add-tag-button:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
} }
.badge-text { .badge-text {
color: #FFFFFF; color: #FFFFFF;
background-color: #FAAB3C; background-color: #FAAB3C;
} }
.badge { .badge {
margin-right: 3px; margin-right: 3px;
} }
.badge-delete { .badge-delete {
margin-right: -2px; margin-right: -2px;
margin-left: 2px; margin-left: 2px;
margin-top: -1px; margin-top: -1px;
font-family: monospace; font-family: monospace;
font-size: 90%; font-size: 90%;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
padding: 0.1em 0.4em; padding: 0.1em 0.4em;
color: white; color: white;
cursor: pointer; cursor: pointer;
} }
.badge-size { .badge-size {
width: 50px; width: 50px;
display: inline-block; display: inline-block;
} }
.add-tag-button { .add-tag-button {
cursor: pointer; cursor: pointer;
color: #212529; color: #212529;
background-color: #e0e0e0; background-color: #e0e0e0;
width: 50px; width: 50px;
} }
.badge { .badge {
user-select: none; user-select: none;
} }
.badge-suggestion { .badge-suggestion {
font-size: 90%; font-size: 90%;
font-weight: normal; font-weight: normal;
} }
</style> </style>
<style> <style>
.vc-twitter-body { .vc-twitter-body {
padding: 0 !important; padding: 0 !important;
} }
.vc-twitter { .vc-twitter {
box-shadow: none !important; box-shadow: none !important;
background: none !important; background: none !important;
} }
.tooltip { .tooltip {
user-select: none; user-select: none;
} }
.toast { .toast {
border: none; border: none;
} }
</style> </style>

View File

@@ -1,13 +1,13 @@
<template> <template>
<div> <div>
<b-input-group v-if="showSearchBar" id="tag-picker-filter-bar"> <b-input-group v-if="showSearchBar" id="tag-picker-filter-bar">
<b-form-input :value="filter" <b-form-input :value="filter"
:placeholder="$t('tagFilter')" :placeholder="$t('tagFilter')"
@input="onFilter($event)"></b-form-input> @input="onFilter($event)"></b-form-input>
</b-input-group> </b-input-group>
<div id="tagTree"></div> <div id="tagTree"></div>
</div> </div>
</template> </template>
<script> <script>
@@ -19,191 +19,199 @@ import {getSelectedTreeNodes} from "@/util";
import Sist2Api from "@/Sist2Api"; import Sist2Api from "@/Sist2Api";
function resetState(node) { function resetState(node) {
node._tree.defaultState.forEach(function (val, prop) { node._tree.defaultState.forEach(function (val, prop) {
node.state(prop, val); node.state(prop, val);
}); });
return node; return node;
} }
function baseStateChange(prop, value, verb, node, deep) { function baseStateChange(prop, value, verb, node, deep) {
if (node.state(prop) !== value) { if (node.state(prop) !== value) {
node._tree.batch(); node._tree.batch();
if (node._tree.config.nodes.resetStateOnRestore && verb === 'restored') { if (node._tree.config.nodes.resetStateOnRestore && verb === 'restored') {
resetState(node); resetState(node);
}
node.state(prop, value);
node._tree.emit('node.' + verb, node, false);
if (deep && node.hasChildren()) {
node.children.recurseDown(function (child) {
baseStateChange(prop, value, verb, child);
});
}
node.markDirty();
node._tree.end();
} }
node.state(prop, value); return node;
node._tree.emit('node.' + verb, node, false);
if (deep && node.hasChildren()) {
node.children.recurseDown(function (child) {
baseStateChange(prop, value, verb, child);
});
}
node.markDirty();
node._tree.end();
}
return node;
} }
function addTag(map, tag, id, count) { function addTag(map, tag, id, count) {
const tags = tag.split("."); const tags = tag.split(".");
const child = { const child = {
id: id, id: id,
count: count, count: count,
text: tags.length !== 1 ? tags[0] : `${tags[0]} (${count})`, text: tags.length !== 1 ? tags[0] : `${tags[0]} (${count})`,
name: tags[0], name: tags[0],
children: [], children: [],
// Overwrite base functions // Overwrite base functions
blur: function () { blur: function () {
// noop // noop
}, },
select: function () { select: function () {
this.state("selected", true); this.state("selected", true);
return this.check() return this.check()
}, },
deselect: function () { deselect: function () {
this.state("selected", false); this.state("selected", false);
return this.uncheck() return this.uncheck()
}, },
uncheck: function () { uncheck: function () {
baseStateChange('checked', false, 'unchecked', this, false); baseStateChange('checked', false, 'unchecked', this, false);
this.state('indeterminate', false); this.state('indeterminate', false);
if (this.hasParent()) { if (this.hasParent()) {
this.getParent().refreshIndeterminateState(); this.getParent().refreshIndeterminateState();
} }
this._tree.end(); this._tree.end();
return this; return this;
}, },
check: function () { check: function () {
baseStateChange('checked', true, 'checked', this, false); baseStateChange('checked', true, 'checked', this, false);
if (this.hasParent()) { if (this.hasParent()) {
this.getParent().refreshIndeterminateState(); this.getParent().refreshIndeterminateState();
} }
this._tree.end(); this._tree.end();
return this; return this;
}
};
let found = false;
map.forEach(node => {
if (node.name === child.name) {
found = true;
if (tags.length !== 1) {
addTag(node.children, tags.slice(1).join("."), id, count);
} else {
// Same name, different color
console.error("FIXME: Duplicate tag?")
console.trace(node)
}
}
});
if (!found) {
if (tags.length !== 1) {
addTag(child.children, tags.slice(1).join("."), id, count);
map.push(child);
} else {
map.push(child);
}
} }
};
let found = false;
map.forEach(node => {
if (node.name === child.name) {
found = true;
if (tags.length !== 1) {
addTag(node.children, tags.slice(1).join("."), id, count);
} else {
// Same name, different color
console.error("FIXME: Duplicate tag?")
console.trace(node)
}
}
});
if (!found) {
if (tags.length !== 1) {
addTag(child.children, tags.slice(1).join("."), id, count);
map.push(child);
} else {
map.push(child);
}
}
} }
export default { export default {
name: "TagPicker", name: "TagPicker",
props: ["showSearchBar"], props: ["showSearchBar"],
data() { data() {
return { return {
tagTree: null, tagTree: null,
loadedFromArgs: false, loadedFromArgs: false,
filter: "" filter: ""
}
},
mounted() {
this.$store.subscribe((mutation) => {
if (mutation.type === "setUiMimeMap" && this.tagTree === null) {
this.initializeTree();
this.updateTree();
} else if (mutation.type === "busUpdateTags") {
window.setTimeout(this.updateTree, 2000);
}
});
},
methods: {
onFilter(value) {
this.filter = value;
this.tagTree.search(value);
},
initializeTree() {
const tagMap = [];
this.tagTree = new InspireTree({
selection: {
mode: "checkbox",
autoDeselect: false,
},
checkbox: {
autoCheckChildren: false,
},
data: tagMap
});
new InspireTreeDOM(this.tagTree, {
target: '#tagTree'
});
this.tagTree.on("node.state.changed", this.handleTreeClick);
},
updateTree() {
// TODO: remember which tags are selected and restore?
const tagMap = [];
Sist2Api.getTags().then(tags => {
tags.forEach(tag => addTag(tagMap, tag.id, tag.id, tag.count));
this.tagTree.removeAll();
this.tagTree.addNodes(tagMap);
if (this.$store.state._onLoadSelectedTags.length > 0 && !this.loadedFromArgs) {
this.$store.state._onLoadSelectedTags.forEach(mime => {
this.tagTree.node(mime).select();
this.loadedFromArgs = true;
});
} }
});
}, },
handleTreeClick(node, e) { mounted() {
if (e === "indeterminate" || e === "collapsed" || e === 'rendered' || e === "focused" this.$store.subscribe((mutation) => {
|| e === "matched" || e === "hidden") { if (mutation.type === "setUiMimeMap" && this.tagTree === null) {
return; this.initializeTree();
} this.updateTree();
} else if (mutation.type === "busUpdateTags") {
if (this.$store.state.uiSqliteMode) {
this.updateTree();
} else {
window.setTimeout(this.updateTree, 2000);
}
}
});
},
methods: {
onFilter(value) {
this.filter = value;
this.tagTree.search(value);
},
initializeTree() {
const tagMap = [];
this.tagTree = new InspireTree({
selection: {
mode: "checkbox",
autoDeselect: false,
},
checkbox: {
autoCheckChildren: false,
},
data: tagMap
});
new InspireTreeDOM(this.tagTree, {
target: '#tagTree'
});
this.tagTree.on("node.state.changed", this.handleTreeClick);
},
updateTree() {
// TODO: remember which tags are selected and restore?
const tagMap = [];
Sist2Api.getTags().then(tags => {
tags.forEach(tag => addTag(tagMap, tag.id, tag.id, tag.count));
this.tagTree.removeAll();
this.tagTree.addNodes(tagMap);
this.$store.commit("setSelectedTags", getSelectedTreeNodes(this.tagTree)); if (this.$store.state._onLoadSelectedTags.length > 0 && !this.loadedFromArgs) {
}, this.$store.state._onLoadSelectedTags.forEach(mime => {
} this.tagTree.node(mime).select();
this.loadedFromArgs = true;
});
}
});
},
handleTreeClick(node, e) {
if (e === "indeterminate" || e === "collapsed" || e === 'rendered' || e === "focused"
|| e === "matched" || e === "hidden") {
return;
}
this.$store.commit("setSelectedTags", getSelectedTreeNodes(this.tagTree));
},
}
} }
</script> </script>
<style scoped> <style scoped>
#mimeTree { #mimeTree {
max-height: 350px; max-height: 350px;
overflow: auto; overflow: auto;
} }
</style> </style>
<style> <style>
.inspire-tree .focused > .wholerow { .inspire-tree .focused > .wholerow {
border: none; border: none;
} }
#tag-picker-filter-bar { #tag-picker-filter-bar {
padding: 10px 4px 4px; padding: 10px 4px 4px;
} }
.theme-black .inspire-tree .matched > .wholerow { .theme-black .inspire-tree .matched > .wholerow {
background: rgba(251, 191, 41, 0.25); background: rgba(251, 191, 41, 0.25);
}
#tagTree {
max-height: 350px;
overflow: auto;
} }
</style> </style>

View File

@@ -0,0 +1,76 @@
<template>
<svg class="ml-icon" :class="{'m-icon': 1, 'ml-icon-big': big, 'ml-icon-clickable': clickable}" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512" xml:space="preserve" fill="currentColor" stroke="currentColor" @click="$emit('click')">
<g>
<path class="st0" d="M167.314,14.993C167.314,6.712,160.602,0,152.332,0h-5.514c-8.27,0-14.982,6.712-14.982,14.993v41.466h35.478
V14.993z"/>
<path class="st0"
d="M238.26,14.993C238.26,6.712,231.549,0,223.278,0h-5.504c-8.271,0-14.982,6.712-14.982,14.993v41.466h35.468 V14.993z"/>
<path class="st0"
d="M309.207,14.993C309.207,6.712,302.496,0,294.225,0h-5.504c-8.271,0-14.982,6.712-14.982,14.993v41.466h35.468 V14.993z"/>
<path class="st0"
d="M380.164,14.993C380.164,6.712,373.453,0,365.182,0h-5.514c-8.27,0-14.982,6.712-14.982,14.993v41.466h35.478 V14.993z"/>
<path class="st0"
d="M131.836,497.007c0,8.282,6.712,14.993,14.982,14.993h5.514c8.27,0,14.982-6.711,14.982-14.993V455.55h-35.478 V497.007z"/>
<path class="st0"
d="M202.792,497.007c0,8.282,6.712,14.993,14.982,14.993h5.504c8.27,0,14.982-6.711,14.982-14.993V455.55h-35.468 V497.007z"/>
<path class="st0"
d="M273.739,497.007c0,8.282,6.712,14.993,14.982,14.993h5.504c8.271,0,14.982-6.711,14.982-14.993V455.55 h-35.468V497.007z"/>
<path class="st0"
d="M344.686,497.007c0,8.282,6.712,14.993,14.982,14.993h5.514c8.271,0,14.982-6.711,14.982-14.993V455.55 h-35.478V497.007z"/>
<path class="st0"
d="M497.018,131.836H455.55v35.479h41.468c8.27,0,14.982-6.712,14.982-14.993v-5.493 C512,138.548,505.288,131.836,497.018,131.836z"/>
<path class="st0"
d="M497.018,202.793H455.55v35.468h41.468c8.27,0,14.982-6.712,14.982-14.982v-5.494 C512,209.504,505.288,202.793,497.018,202.793z"/>
<path class="st0"
d="M497.018,273.739H455.55v35.468h41.468c8.27,0,14.982-6.711,14.982-14.992v-5.494 C512,280.451,505.288,273.739,497.018,273.739z"/>
<path class="st0"
d="M497.018,344.686H455.55v35.479h41.468c8.27,0,14.982-6.712,14.982-14.993v-5.493 C512,351.398,505.288,344.686,497.018,344.686z"/>
<path class="st0"
d="M0,146.828v5.493c0,8.281,6.711,14.993,14.982,14.993H56.46v-35.479H14.982C6.711,131.836,0,138.548,0,146.828 z"/>
<path class="st0"
d="M0,217.785v5.494c0,8.27,6.711,14.982,14.982,14.982H56.46v-35.468H14.982C6.711,202.793,0,209.504,0,217.785z "/>
<path class="st0"
d="M0,288.721v5.494c0,8.281,6.711,14.992,14.982,14.992H56.46v-35.468H14.982C6.711,273.739,0,280.451,0,288.721 z"/>
<path class="st0"
d="M0,359.679v5.493c0,8.281,6.711,14.993,14.982,14.993H56.46v-35.479H14.982C6.711,344.686,0,351.398,0,359.679 z"/>
<path class="st0"
d="M78.628,433.382h354.753V78.628H78.628V433.382z M376.56,120.2c9.18,0,16.635,7.445,16.635,16.634 c0,9.18-7.455,16.624-16.635,16.624c-9.179,0-16.624-7.445-16.624-16.624C359.936,127.644,367.381,120.2,376.56,120.2z M376.56,361.32c9.18,0,16.635,7.445,16.635,16.635c0,9.179-7.455,16.623-16.635,16.623c-9.179,0-16.624-7.444-16.624-16.623 C359.936,368.764,367.381,361.32,376.56,361.32z M184.362,184.362h143.287v143.287H184.362V184.362z M135.439,120.2 c9.19,0,16.635,7.445,16.635,16.634c0,9.169-7.445,16.624-16.635,16.624c-9.178,0-16.623-7.455-16.623-16.624 C118.816,127.644,126.26,120.2,135.439,120.2z M135.439,361.32c9.19,0,16.635,7.445,16.635,16.635 c0,9.169-7.445,16.623-16.635,16.623c-9.178,0-16.623-7.454-16.623-16.623C118.816,368.764,126.26,361.32,135.439,361.32z"/>
</g>
</svg>
</template>
<script>
export default {
name: "MLIcon",
props: {
"big": Boolean,
"clickable": Boolean
}
}
</script>
<style scoped>
.ml-icon-clickable {
cursor: pointer;
}
.ml-icon-big {
width: 24px !important;
height: 24px !important;
}
.ml-icon {
width: 1rem;
min-width: 1rem;
margin-right: 0.2rem;
line-height: 1rem;
height: 1rem;
min-height: 1rem;
filter: brightness(45%);
}
.theme-black .ml-icon {
filter: brightness(80%);
}
</style>

View File

@@ -1,7 +1,7 @@
export default { export default {
en: { en: {
filePage: { filePage: {
notFound: "Not found" notFound: "Not found"
}, },
searchBar: { searchBar: {
simple: "Search", simple: "Search",
@@ -18,6 +18,7 @@ export default {
tags: "Tags", tags: "Tags",
tagFilter: "Filter tags", tagFilter: "Filter tags",
forExample: "For example:", forExample: "For example:",
embeddingsSearchPlaceholder: "Embeddings search",
help: { help: {
simpleSearch: "Simple search", simpleSearch: "Simple search",
advancedSearch: "Advanced search", advancedSearch: "Advanced search",
@@ -49,6 +50,7 @@ export default {
configReset: "Reset configuration", configReset: "Reset configuration",
searchOptions: "Search options", searchOptions: "Search options",
treemapOptions: "Treemap options", treemapOptions: "Treemap options",
mlOptions: "Machine learning options",
displayOptions: "Display options", displayOptions: "Display options",
opt: { opt: {
lang: "Language", lang: "Language",
@@ -56,7 +58,7 @@ export default {
fuzzy: "Set fuzzy search by default", fuzzy: "Set fuzzy search by default",
searchInPath: "Enable matching query against document path", searchInPath: "Enable matching query against document path",
suggestPath: "Enable auto-complete in path filter bar", suggestPath: "Enable auto-complete in path filter bar",
fragmentSize: "Highlight context size in characters", fragmentSize: "Highlight context size",
queryMode: "Search mode", queryMode: "Search mode",
displayMode: "Display", displayMode: "Display",
columns: "Column count", columns: "Column count",
@@ -78,7 +80,10 @@ export default {
simpleLightbox: "Disable animations in image viewer", simpleLightbox: "Disable animations in image viewer",
showTagPickerFilter: "Display the tag filter bar", showTagPickerFilter: "Display the tag filter bar",
featuredFields: "Featured fields Javascript template string. Will appear in the search results.", featuredFields: "Featured fields Javascript template string. Will appear in the search results.",
featuredFieldsList: "Available variables" featuredFieldsList: "Available variables",
autoAnalyze: "Automatically analyze text",
defaultModel: "Default model",
mlRepositories: "Model repositories (one per line)"
}, },
queryMode: { queryMode: {
simple: "Simple", simple: "Simple",
@@ -88,6 +93,7 @@ export default {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",
fr: "Français", fr: "Français",
pl: "Polski",
"zh-CN": "简体中文", "zh-CN": "简体中文",
}, },
displayMode: { displayMode: {
@@ -171,10 +177,16 @@ export default {
selectedIndex: "selected index", selectedIndex: "selected index",
selectedIndices: "selected indices", selectedIndices: "selected indices",
}, },
ml: {
analyzeText: "Analyze",
auto: "Auto",
repoFetchError: "Failed to get list of models. Check browser console for more details.",
repoFetchErrorTitle: "Could not fetch model repositories",
}
}, },
de: { de: {
filePage: { filePage: {
notFound: "Nicht gefunden" notFound: "Nicht gefunden"
}, },
searchBar: { searchBar: {
simple: "Suche", simple: "Suche",
@@ -229,7 +241,7 @@ export default {
fuzzy: "Aktiviere Fuzzy-Suche standardmäßig", fuzzy: "Aktiviere Fuzzy-Suche standardmäßig",
searchInPath: "Abgleich der Abfrage mit dem Dokumentpfad aktivieren", searchInPath: "Abgleich der Abfrage mit dem Dokumentpfad aktivieren",
suggestPath: "Aktiviere Auto-Vervollständigung in Pfadfilter-Leiste", suggestPath: "Aktiviere Auto-Vervollständigung in Pfadfilter-Leiste",
fragmentSize: "Kontextgröße in Zeichen hervorheben", fragmentSize: "Kontextgröße",
queryMode: "Such-Modus", queryMode: "Such-Modus",
displayMode: "Ansicht", displayMode: "Ansicht",
columns: "Anzahl Spalten", columns: "Anzahl Spalten",
@@ -250,8 +262,8 @@ export default {
vidPreviewInterval: "Videovorschau Framedauer in ms", vidPreviewInterval: "Videovorschau Framedauer in ms",
simpleLightbox: "Schalte Animationen im Image-Viewer ab", simpleLightbox: "Schalte Animationen im Image-Viewer ab",
showTagPickerFilter: "Zeige die Tag-Filter-Leiste", showTagPickerFilter: "Zeige die Tag-Filter-Leiste",
featuredFields: "Ausgewählte Felder Javascript Vorlage String. Wird in den Suchergebnissen angezeigt.", featuredFields: "Variablen, welche zusätzlich in den Suchergebnissen angezeigt werden können.",
featuredFieldsList: "Verfügbare Variablen" featuredFieldsList: "verfügbare Variablen"
}, },
queryMode: { queryMode: {
simple: "Einfach", simple: "Einfach",
@@ -261,6 +273,7 @@ export default {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",
fr: "Français", fr: "Français",
pl: "Polski",
"zh-CN": "简体中文", "zh-CN": "简体中文",
}, },
displayMode: { displayMode: {
@@ -333,10 +346,10 @@ export default {
random: "zufällig", random: "zufällig",
}, },
d3: { d3: {
mimeCount: "Anzahlverteilung nach Medientyp", mimeCount: "Anzahl nach Medientyp",
mimeSize: "Größenverteilung nach Medientyp", mimeSize: "Größen nach Medientyp",
dateHistogram: "Verteilung der Änderungszeiten", dateHistogram: "Änderungszeiten",
sizeHistogram: "Verteilung der Dateigrößen", sizeHistogram: "Dateigrößen",
}, },
indexPicker: { indexPicker: {
selectNone: "keinen auswählen", selectNone: "keinen auswählen",
@@ -403,7 +416,7 @@ export default {
fuzzy: "Activer la recherche approximative par défaut", fuzzy: "Activer la recherche approximative par défaut",
searchInPath: "Activer la recherche dans le chemin des documents", searchInPath: "Activer la recherche dans le chemin des documents",
suggestPath: "Activer l'autocomplétion dans la barre de filtre de chemin", suggestPath: "Activer l'autocomplétion dans la barre de filtre de chemin",
fragmentSize: "Longueur du contexte de surlignage, en nombre de caractères", fragmentSize: "Longueur du contexte de surlignage",
queryMode: "Mode de recherche", queryMode: "Mode de recherche",
displayMode: "Affichage", displayMode: "Affichage",
columns: "Nombre de colonnes", columns: "Nombre de colonnes",
@@ -435,6 +448,7 @@ export default {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",
fr: "Français", fr: "Français",
pl: "Polski",
"zh-CN": "简体中文", "zh-CN": "简体中文",
}, },
displayMode: { displayMode: {
@@ -609,6 +623,7 @@ export default {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",
fr: "Français", fr: "Français",
pl: "Polski",
"zh-CN": "简体中文", "zh-CN": "简体中文",
}, },
displayMode: { displayMode: {
@@ -693,4 +708,188 @@ export default {
selectedIndices: "选中索引", selectedIndices: "选中索引",
}, },
}, },
pl: {
filePage: {
notFound: "Nie znaleziono"
},
searchBar: {
simple: "Szukaj",
advanced: "Zaawansowane szukanie",
fuzzy: "Również podobne"
},
addTag: "Tag",
deleteTag: "Usuń",
download: "Pobierz",
and: "i",
page: "strona",
pages: "stron",
mimeTypes: "Typy danych",
tags: "Tagi",
tagFilter: "Filtruj tagi",
forExample: "Na przykład:",
help: {
simpleSearch: "Proste szukanie",
advancedSearch: "Zaawansowane szukanie",
help: "Pomoc",
term: "<WYRAZ>",
and: "operator I",
or: "operator LUB",
not: "zabrania danego wyrazu",
quotes: "znajdzie objętą sekwencję wyrazów w podanej kolejności",
prefix: "znajdzie dowolny wyraz rozpoczynający się na takie litery, jeśli zastosowane na końcu wyrazu",
parens: "używane do grupowania wyrażeń",
tildeTerm: "znajdzie wyraz w podanej odległości",
tildePhrase: "znajdzie frazę przeplecioną podaną liczbą niepasujących wyrazów",
example1:
"Na przykład: <code>\"pieczone jajko\" +(kiełbasa | ziemniak) -frytki</code> znajdzie frazę " +
"<i>pieczone jajko</i> gdzie występuje też: <i>kiełbasa</i> albo <i>ziemniak</i>, ale zignoruje rezultat " +
"zawierający <i>frytki</i>.",
defaultOperator:
"Kiedy nie podano ani <code>+</code>, ani <code>|</code>, to domyślnym operatorem jest " +
"<code>+</code> (i).",
fuzzy:
"Kiedy opcja <b>Również podobne</b> jest zaznaczona, częściowo zgodne wyrazy są również znajdywane.",
moreInfoSimple: "Po więcej informacji sięgnij do <a target=\"_blank\" " +
"rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html\">dokumentacji Elasticsearch</a>",
moreInfoAdvanced: "Aby uzyskać więcej informacji o zaawansowanym szukaniu, przeczytaj <a target=\"_blank\" rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax\">dokumentację Elasticsearch</a>"
},
config: "Ustawienia",
configDescription: "Ustawienia są zapisywane na żywo w tej przeglądarce.",
configReset: "Zresetuj ustawienia",
searchOptions: "Opcje szukania",
treemapOptions: "Opcje mapy",
mlOptions: "Opcje uczenia maszynowego",
displayOptions: "Opcje wyświetlania",
opt: {
lang: "Język",
highlight: "Zaznaczaj znalezione fragmenty",
fuzzy: "Ustaw szukanie również podobnych jako domyślne",
searchInPath: "Włącz szukanie również w ścieżce dokumentu",
suggestPath: "Włącz auto-uzupełnianie w filtrze ścieżek",
fragmentSize: "Podświetl wielkość kontekstu w znakach",
queryMode: "Tryb szukania",
displayMode: "Wyświetlanie",
columns: "Liczba kolumn",
treemapType: "Typ mapy",
treemapTiling: "Układanie mapy",
treemapColorGroupingDepth: "Jak głęboko grupować kolory mapy (na płasko)",
treemapColor: "Kolor mapy (kaskadowo)",
treemapSize: "Wielkość mapy",
theme: "Styl graficzny",
lightboxLoadOnlyCurrent: "Nie pobieraj od razu obrazów w pełnej wielkości dla sąsiednich obrazów podglądu.",
slideDuration: "Czas trwania jednego slajdu w pokazie slajdów",
resultSize: "Liczba wyników na stronę",
tagOrOperator: "Użyj operatora LUB przy wyborze kilku tagów",
hideDuplicates: "Ukryj zduplikowane wyniki (według sumy kontrolnej)",
hideLegacy: "Ukryj powiadomienie Elasticsearch 'legacyES'",
updateMimeMap: "Uaktualniaj drzewo typów mediów na żywo",
useDatePicker: "Używaj kalendarza do wyboru dat, zamiast suwaka",
vidPreviewInterval: "Czas trwania jednej klatki w podglądzie wideo (w ms)",
simpleLightbox: "Wyłącz animacje w podglądzie obrazów",
showTagPickerFilter: "Pokazuj pole filtrowania tagów",
featuredFields: "Wybrane pola szablonu Javascript. Będą pojawiać się przy wynikach wyszukiwania.",
featuredFieldsList: "Dostępne zmienne",
autoAnalyze: "Automatycznie analizuj tekst",
defaultModel: "Domyślny model",
mlRepositories: "Repozytoria modeli (każde w osobnej linii)"
},
queryMode: {
simple: "Proste",
advanced: "Zaawansowane",
},
lang: {
en: "English",
de: "Deutsch",
fr: "Français",
pl: "Polski",
"zh-CN": "简体中文",
},
displayMode: {
grid: "Siatka",
list: "Lista",
},
columns: {
auto: "Automatyczna"
},
treemapType: {
cascaded: "Kaskadowa",
flat: "Płaska (kompaktowa)"
},
treemapSize: {
small: "Mała",
medium: "Średnia",
large: "Duża",
xLarge: "Bardzo duża",
xxLarge: "Ogromna",
custom: "Inna",
},
treemapTiling: {
binary: "Binarnie",
squarify: "Kwadratowo",
slice: "Wycinek",
dice: "Kostka",
sliceDice: "Wycinek i kostka",
},
theme: {
light: "Jasny",
black: "Czarny"
},
hit: "traf",
hits: "trafień",
details: "Szczegóły",
stats: "Statystyki",
queryTime: "Czas szukania",
totalSize: "Całkowita wielkość",
pathBar: {
placeholder: "Filtruj ścieżki",
modalTitle: "Wybierz ścieżkę"
},
debug: "Informacje dla programistów",
debugDescription: "Informacje przydatne do znajdowania błędów w oprogramowaniu. Jeśli napotkasz błąd lub masz" +
" propozycje zmian, zgłoś to proszę <a href='https://github.com/simon987/sist2/issues/new/choose'>tutaj</a>.",
tagline: "Slogan",
toast: {
esConnErrTitle: "Problem z połączeniem z Elasticsearch",
esConnErr: "Moduł strony internetowej sist2 napotkał problem przy połączeniu z Elasticsearch." +
" Zobacz logi serwera, aby uzyskać więcej informacji.",
esQueryErrTitle: "Problem z kwerendą",
esQueryErr: "Kwerenda szukania jest niezrozumiała albo nie udało się jej przesłać. Sprawdź dokumentację zaawansowanego szukania. " +
"Zobacz logi serwera, aby uzyskać więcej informacji.",
dupeTagTitle: "Zduplikowany tag",
dupeTag: "Ten dokument już ma taki tag.",
copiedToClipboard: "Skopiowano do schowka"
},
saveTagModalTitle: "Dodaj tag",
saveTagPlaceholder: "Nazwa",
confirm: "Zatwierdź",
indexPickerPlaceholder: "Wybierz indeks",
sort: {
relevance: "Zgodność z szukanym",
dateAsc: "Data (najpierw starsze)",
dateDesc: "Data (najpierw nowsze)",
sizeAsc: "Wielkość (najpierw mniejsze)",
sizeDesc: "Wielkość (najpierw większe)",
nameAsc: "Nazwa (A-z)",
nameDesc: "Nazwa (Z-a)",
random: "Losowo",
},
d3: {
mimeCount: "Dystrybucja liczby plików według typów mediów",
mimeSize: "Dystrybucja wielkości plików według typów mediów",
dateHistogram: "Dystrybucja dat modyfikacji plików",
sizeHistogram: "Dystrybucja wielkości plików",
},
indexPicker: {
selectNone: "Zaznacz nic",
selectAll: "Zaznacz wszystko",
selectedIndex: "wybrany indeks",
selectedIndices: "wybrane indeksy",
},
ml: {
analyzeText: "Analizuj",
auto: "Automatycznie",
repoFetchError: "Nie udało się uzyskać listy modeli. Zobacz konsolę przeglądarki, aby uzyskać więcej informacji.",
repoFetchErrorTitle: "Nie udało się pobrać repozytoriów modeli",
}
}
} }

View File

@@ -1,4 +1,3 @@
import '@babel/polyfill'
import 'mutationobserver-shim' import 'mutationobserver-shim'
import Vue from 'vue' import Vue from 'vue'
import './plugins/bootstrap-vue' import './plugins/bootstrap-vue'

View File

@@ -0,0 +1,118 @@
const inf = Number.POSITIVE_INFINITY;
const START_TOK = 49406;
const END_TOK = 49407;
function min(array, key) {
return array
.reduce((a, b) => (key(a, b) ? b : a))
}
class TupleSet extends Set {
add(elem) {
return super.add(elem.join("`"));
}
has(elem) {
return super.has(elem.join("`"));
}
toList() {
return [...this].map(x => x.split("`"))
}
}
export class BPETokenizer {
_encoder = null;
_bpeRanks = null;
constructor(encoder, bpeRanks) {
this._encoder = encoder;
this._bpeRanks = bpeRanks;
}
getPairs(word) {
const pairs = new TupleSet();
let prevChar = word[0];
for (let i = 1; i < word.length; i++) {
pairs.add([prevChar, word[i]])
prevChar = word[i];
}
return pairs.toList();
}
bpe(token) {
let word = [...token];
word[word.length - 1] += "</w>";
let pairs = this.getPairs(word)
if (pairs.length === 0) {
return token + "</w>"
}
while (true) {
const bigram = min(pairs, (a, b) => {
return (this._bpeRanks[a.join("`")] ?? inf) > (this._bpeRanks[b.join("`") ?? inf])
});
if (this._bpeRanks[bigram.join("`")] === undefined) {
break;
}
const [first, second] = bigram;
let newWord = [];
let i = 0;
while (i < word.length) {
const j = word.indexOf(first, i);
if (j === -1) {
newWord.push(...word.slice(i));
break;
} else {
newWord.push(...word.slice(i, j));
i = j;
}
if (word[i] === first && i < word.length - 1 && word[i + 1] === second) {
newWord.push(first + second);
i += 2;
} else {
newWord.push(word[i]);
i += 1;
}
}
word = [...newWord]
if (word.length === 1) {
break;
} else {
pairs = this.getPairs(word);
}
}
return word.join(" ");
}
encode(text) {
let bpeTokens = [];
text = text.trim();
text = text.replaceAll(/\s+/g, " ");
text
.match(/<\|startoftext\|>|<\|endoftext\|>|'s|'t|'re|'ve|'m|'ll|'d|[a-zA-Z0-9]+/ig)
.forEach(token => {
bpeTokens.push(...this.bpe(token).split(" ").map(t => this._encoder[t]));
});
bpeTokens.unshift(START_TOK);
bpeTokens = bpeTokens.slice(0, 76);
bpeTokens.push(END_TOK);
while (bpeTokens.length < 77) {
bpeTokens.push(0);
}
return bpeTokens;
}
}

View File

@@ -0,0 +1,89 @@
import BertTokenizer from "@/ml/BertTokenizer";
import axios from "axios";
import {chunk as _chunk} from "underscore";
import * as ort from "onnxruntime-web";
import {argMax, downloadToBuffer, ORT_WASM_PATHS} from "@/ml/mlUtils";
export default class BertNerModel {
vocabUrl;
modelUrl;
id2label;
_tokenizer;
_model;
inputSize = 128;
_previousWordId = null;
constructor(vocabUrl, modelUrl, id2label) {
this.vocabUrl = vocabUrl;
this.modelUrl = modelUrl;
this.id2label = id2label;
}
async init(onProgress) {
await Promise.all([this.loadTokenizer(), this.loadModel(onProgress)]);
}
async loadTokenizer() {
const vocab = (await axios.get(this.vocabUrl)).data;
this._tokenizer = new BertTokenizer(vocab);
}
async loadModel(onProgress) {
ort.env.wasm.wasmPaths = ORT_WASM_PATHS;
const buf = await downloadToBuffer(this.modelUrl, onProgress);
this._model = await ort.InferenceSession.create(buf.buffer, {executionProviders: ["wasm"]});
}
alignLabels(labels, wordIds, words) {
const result = [];
for (let i = 0; i < this.inputSize; i++) {
const label = labels[i];
const wordId = wordIds[i];
if (wordId === -1) {
continue;
}
if (wordId === this._previousWordId) {
continue;
}
result.push({
word: words[wordId].text, wordIndex: words[wordId].index, label: label
});
this._previousWordId = wordId;
}
return result;
}
async predict(text, callback) {
this._previousWordId = null;
const encoded = this._tokenizer.encodeText(text, this.inputSize);
let i = 0;
for (let chunk of encoded.inputChunks) {
const results = await this._model.run({
input_ids: new ort.Tensor("int32", chunk.inputIds, [1, this.inputSize]),
token_type_ids: new ort.Tensor("int32", chunk.segmentIds, [1, this.inputSize]),
attention_mask: new ort.Tensor("int32", chunk.inputMask, [1, this.inputSize]),
});
const labelIds = _chunk(results["output"].data, this.id2label.length).map(argMax);
const labels = labelIds.map(id => this.id2label[id]);
callback(this.alignLabels(labels, chunk.wordIds, encoded.words));
i += 1;
// give browser some time to repaint
if (i % 2 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
}

View File

@@ -0,0 +1,185 @@
import {zip, chunk} from "underscore";
import {toInt64} from "@/ml/mlUtils";
const UNK_INDEX = 100;
const CLS_INDEX = 101;
const SEP_INDEX = 102;
const CONTINUING_SUBWORD_PREFIX = "##";
function isWhitespace(ch) {
return /\s/.test(ch);
}
function isInvalid(ch) {
return (ch.charCodeAt(0) === 0 || ch.charCodeAt(0) === 0xfffd);
}
const punctuations = '[~`!@#$%^&*(){}[];:"\'<,.>?/\\|-_+=';
/** To judge whether it's a punctuation. */
function isPunctuation(ch) {
return punctuations.indexOf(ch) !== -1;
}
export default class BertTokenizer {
vocab;
constructor(vocab) {
this.vocab = vocab;
}
tokenize(text) {
const charOriginalIndex = [];
const cleanedText = this.cleanText(text, charOriginalIndex);
const origTokens = cleanedText.split(' ');
let charCount = 0;
const tokens = origTokens.map((token) => {
token = token.toLowerCase();
const tokens = this.runSplitOnPunctuation(token, charCount, charOriginalIndex);
charCount += token.length + 1;
return tokens;
});
let flattenTokens = [];
for (let index = 0; index < tokens.length; index++) {
flattenTokens = flattenTokens.concat(tokens[index]);
}
return flattenTokens;
}
/* Performs invalid character removal and whitespace cleanup on text. */
cleanText(text, charOriginalIndex) {
text = text.replace(/\?/g, "").trim();
const stringBuilder = [];
let originalCharIndex = 0;
let newCharIndex = 0;
for (const ch of text) {
// Skip the characters that cannot be used.
if (isInvalid(ch)) {
originalCharIndex += ch.length;
continue;
}
if (isWhitespace(ch)) {
if (stringBuilder.length > 0 && stringBuilder[stringBuilder.length - 1] !== ' ') {
stringBuilder.push(' ');
charOriginalIndex[newCharIndex] = originalCharIndex;
originalCharIndex += ch.length;
} else {
originalCharIndex += ch.length;
continue;
}
} else {
stringBuilder.push(ch);
charOriginalIndex[newCharIndex] = originalCharIndex;
originalCharIndex += ch.length;
}
newCharIndex++;
}
return stringBuilder.join('');
}
/* Splits punctuation on a piece of text. */
runSplitOnPunctuation(text, count, charOriginalIndex) {
const tokens = [];
let startNewWord = true;
for (const ch of text) {
if (isPunctuation(ch)) {
tokens.push({text: ch, index: charOriginalIndex[count]});
count += ch.length;
startNewWord = true;
} else {
if (startNewWord) {
tokens.push({text: '', index: charOriginalIndex[count]});
startNewWord = false;
}
tokens[tokens.length - 1].text += ch;
count += ch.length;
}
}
return tokens;
}
encode(words) {
let outputTokens = [];
const wordIds = [];
for (let i = 0; i < words.length; i++) {
let chars = [...words[i].text];
let isUnknown = false;
let start = 0;
let subTokens = [];
while (start < chars.length) {
let end = chars.length;
let currentSubstring = null;
while (start < end) {
let substr = chars.slice(start, end).join('');
if (start > 0) {
substr = CONTINUING_SUBWORD_PREFIX + substr;
}
if (this.vocab.includes(substr)) {
currentSubstring = this.vocab.indexOf(substr);
break;
}
--end;
}
if (currentSubstring == null) {
isUnknown = true;
break;
}
subTokens.push(currentSubstring);
start = end;
}
if (isUnknown) {
outputTokens.push(UNK_INDEX);
wordIds.push(i);
} else {
subTokens.forEach(tok => {
outputTokens.push(tok);
wordIds.push(i)
});
}
}
return {tokens: outputTokens, wordIds};
}
encodeText(inputText, inputSize) {
const tokenized = this.tokenize(inputText);
const encoded = this.encode(tokenized);
const encodedTokenChunks = chunk(encoded.tokens, inputSize - 2);
const encodedWordIdChunks = chunk(encoded.wordIds, inputSize - 2);
const chunks = [];
zip(encodedTokenChunks, encodedWordIdChunks).forEach(([tokens, wordIds]) => {
const inputIds = [CLS_INDEX, ...tokens, SEP_INDEX];
const segmentIds = Array(inputIds.length).fill(0);
const inputMask = Array(inputIds.length).fill(1);
wordIds = [-1, ...wordIds, -1];
while (inputIds.length < inputSize) {
inputIds.push(0);
inputMask.push(0);
segmentIds.push(0);
wordIds.push(-1);
}
chunks.push({inputIds, inputMask, segmentIds, wordIds})
});
return {
inputChunks: chunks,
words: tokenized
};
}
}

View File

@@ -0,0 +1,57 @@
import * as ort from "onnxruntime-web";
import {BPETokenizer} from "@/ml/BPETokenizer";
import axios from "axios";
import {downloadToBuffer, ORT_WASM_PATHS} from "@/ml/mlUtils";
import ModelStore from "@/ml/ModelStore";
export class CLIPTransformerModel {
_modelUrl = null;
_tokenizerUrl = null;
_model = null;
_tokenizer = null;
constructor(modelUrl, tokenizerUrl) {
this._modelUrl = modelUrl;
this._tokenizerUrl = tokenizerUrl;
}
async init(onProgress) {
await Promise.all([this.loadTokenizer(), this.loadModel(onProgress)]);
}
async loadModel(onProgress) {
ort.env.wasm.wasmPaths = ORT_WASM_PATHS;
ort.env.wasm.numThreads = 2;
let buf = await ModelStore.get(this._modelUrl);
if (!buf) {
buf = await downloadToBuffer(this._modelUrl, onProgress);
await ModelStore.set(this._modelUrl, buf);
}
this._model = await ort.InferenceSession.create(buf.buffer, {
executionProviders: ["wasm"],
});
}
async loadTokenizer() {
const resp = await axios.get(this._tokenizerUrl);
this._tokenizer = new BPETokenizer(resp.data.encoder, resp.data.bpe_ranks)
}
async predict(text) {
const tokenized = this._tokenizer.encode(text);
const inputs = {
input_ids: new ort.Tensor("int32", tokenized, [1, 77])
};
const results = await this._model.run(inputs);
return Array.from(
Object.values(results)
.find(result => result.size === 512).data
);
}
}

View File

@@ -0,0 +1,67 @@
class ModelStore {
_ok;
_db;
_resolve;
_loadingPromise;
constructor() {
const request = window.indexedDB.open("ModelStore", 1);
request.onerror = () => {
this._ok = false;
}
request.onupgradeneeded = event => {
const db = event.target.result;
db.createObjectStore("models");
}
request.onsuccess = () => {
this._ok = true;
this._db = request.result;
this._resolve();
}
this._loadingPromise = new Promise(resolve => this._resolve = resolve);
}
async get(key) {
await this._loadingPromise;
const req = this._db.transaction(["models"], "readwrite")
.objectStore("models")
.get(key);
return new Promise(resolve => {
req.onsuccess = event => {
resolve(event.target.result);
};
req.onerror = event => {
console.log("ERROR:");
console.log(event);
resolve(null);
};
});
}
async set(key, val) {
await this._loadingPromise;
const req = this._db.transaction(["models"], "readwrite")
.objectStore("models")
.put(val, key);
return new Promise(resolve => {
req.onsuccess = () => {
resolve(true);
};
req.onerror = () => {
resolve(false);
};
});
}
}
export default new ModelStore();

View File

@@ -0,0 +1,46 @@
export async function downloadToBuffer(url, onProgress) {
const resp = await fetch(url);
const contentLength = +resp.headers.get("Content-Length");
const buf = new Uint8ClampedArray(contentLength);
const reader = resp.body.getReader();
let cursor = 0;
if (onProgress) {
onProgress(0);
}
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
buf.set(value, cursor);
cursor += value.length;
if (onProgress) {
onProgress(cursor / contentLength);
}
}
return buf;
}
export function argMax(array) {
return array
.map((x, i) => [x, i])
.reduce((r, a) => (a[0] > r[0] ? a : r))[1];
}
export function toInt64(array) {
return new BigInt64Array(array.map(BigInt));
}
export const ORT_WASM_PATHS = {
"ort-wasm-simd.wasm": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.15.1/dist/ort-wasm-simd.wasm",
"ort-wasm.wasm": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.15.1/dist/ort-wasm.wasm",
"ort-wasm-simd-threaded.wasm": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.15.1/dist/ort-wasm-simd-threaded.wasm",
"ort-wasm-threaded.wasm": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.15.1/dist/ort-wasm-threaded.wasm",
}

View File

@@ -0,0 +1,43 @@
import axios from "axios";
class ModelsRepo {
_repositories;
data = {};
async init(repositories) {
this._repositories = repositories;
const data = await Promise.all(this._repositories.map(this._loadRepository));
data.forEach(models => {
models.forEach(model => {
this.data[model.name] = model;
})
});
}
async _loadRepository(repository) {
const data = (await axios.get(repository)).data;
data.forEach(model => {
model["modelUrl"] = new URL(model["modelPath"], repository).href;
model["vocabUrl"] = new URL(model["vocabPath"], repository).href;
});
return data;
}
getOptions() {
return Object.values(this.data).map(model => ({
text: `${model.name} (${Math.round(model.size / (1024*1024))}MB)`,
value: model.name
}));
}
getDefaultModel() {
if (Object.values(this.data).length === 0) {
return null;
}
return Object.values(this.data).find(model => model.default).name;
}
}
export default new ModelsRepo();

View File

@@ -1,11 +1,11 @@
import Vue from "vue" import Vue from "vue"
import Vuex from "vuex" import Vuex from "vuex"
import VueRouter, {Route} from "vue-router"; import VueRouter, {Route} from "vue-router";
import {EsHit, EsResult, EsTag, Index, Tag} from "@/Sist2Api"; import {EsHit, EsResult, EsTag, Index} from "@/Sist2Api";
import {deserializeMimes, randomSeed, serializeMimes} from "@/util"; import {deserializeMimes, randomSeed, serializeMimes} from "@/util";
import {getInstance} from "@/plugins/auth0.js"; import {getInstance} from "@/plugins/auth0.js";
const CONF_VERSION = 2; const CONF_VERSION = 3;
Vue.use(Vuex) Vue.use(Vuex)
@@ -23,6 +23,9 @@ export default new Vuex.Store({
dateMin: undefined, dateMin: undefined,
dateMax: undefined, dateMax: undefined,
searchText: "", searchText: "",
embeddingText: "",
embedding: null,
embeddingDoc: null,
pathText: "", pathText: "",
sortMode: "score", sortMode: "score",
@@ -57,6 +60,9 @@ export default new Vuex.Store({
optVidPreviewInterval: 700, optVidPreviewInterval: 700,
optSimpleLightbox: true, optSimpleLightbox: true,
optShowTagPickerFilter: true, optShowTagPickerFilter: true,
optMlRepositories: "https://raw.githubusercontent.com/simon987/sist2-ner-models/main/repo.json",
optAutoAnalyze: false,
optMlDefaultModel: null,
_onLoadSelectedIndices: [] as string[], _onLoadSelectedIndices: [] as string[],
_onLoadSelectedMimeTypes: [] as string[], _onLoadSelectedMimeTypes: [] as string[],
@@ -66,11 +72,12 @@ export default new Vuex.Store({
selectedTags: [] as string[], selectedTags: [] as string[],
lastQueryResults: null, lastQueryResults: null,
firstQueryResults: null,
keySequence: 0, keySequence: 0,
querySequence: 0, querySequence: 0,
uiTagHover: null as Tag | null, uiSqliteMode: false,
uiLightboxIsOpen: false, uiLightboxIsOpen: false,
uiShowLightbox: false, uiShowLightbox: false,
uiLightboxSources: [] as string[], uiLightboxSources: [] as string[],
@@ -86,7 +93,12 @@ export default new Vuex.Store({
uiMimeMap: [] as any[], uiMimeMap: [] as any[],
auth0Token: null auth0Token: null,
nerModel: {
model: null,
name: null
},
embeddingsModel: null
}, },
mutations: { mutations: {
setUiShowDetails: (state, val) => state.uiShowDetails = val, setUiShowDetails: (state, val) => state.uiShowDetails = val,
@@ -121,15 +133,18 @@ export default new Vuex.Store({
setDateBoundsMin: (state, val) => state.dateBoundsMin = val, setDateBoundsMin: (state, val) => state.dateBoundsMin = val,
setDateBoundsMax: (state, val) => state.dateBoundsMax = val, setDateBoundsMax: (state, val) => state.dateBoundsMax = val,
setSearchText: (state, val) => state.searchText = val, setSearchText: (state, val) => state.searchText = val,
setEmbeddingText: (state, val) => state.embeddingText = val,
setEmbedding: (state, val) => state.embedding = val,
setEmbeddingDoc: (state, val) => state.embeddingDoc = val,
setFuzzy: (state, val) => state.fuzzy = val, setFuzzy: (state, val) => state.fuzzy = val,
setLastQueryResult: (state, val) => state.lastQueryResults = val, setLastQueryResult: (state, val) => state.lastQueryResults = val,
setFirstQueryResult: (state, val) => state.firstQueryResults = val,
_setOnLoadSelectedIndices: (state, val) => state._onLoadSelectedIndices = val, _setOnLoadSelectedIndices: (state, val) => state._onLoadSelectedIndices = val,
_setOnLoadSelectedMimeTypes: (state, val) => state._onLoadSelectedMimeTypes = val, _setOnLoadSelectedMimeTypes: (state, val) => state._onLoadSelectedMimeTypes = val,
_setOnLoadSelectedTags: (state, val) => state._onLoadSelectedTags = val, _setOnLoadSelectedTags: (state, val) => state._onLoadSelectedTags = val,
setSelectedIndices: (state, val) => state.selectedIndices = val, setSelectedIndices: (state, val) => state.selectedIndices = val,
setSelectedMimeTypes: (state, val) => state.selectedMimeTypes = val, setSelectedMimeTypes: (state, val) => state.selectedMimeTypes = val,
setSelectedTags: (state, val) => state.selectedTags = val, setSelectedTags: (state, val) => state.selectedTags = val,
setUiTagHover: (state, val: Tag | null) => state.uiTagHover = val,
setUiLightboxIsOpen: (state, val: boolean) => state.uiLightboxIsOpen = val, setUiLightboxIsOpen: (state, val: boolean) => state.uiLightboxIsOpen = val,
_setUiShowLightbox: (state, val: boolean) => state.uiShowLightbox = val, _setUiShowLightbox: (state, val: boolean) => state.uiShowLightbox = val,
setUiLightboxKey: (state, val: number) => state.uiLightboxKey = val, setUiLightboxKey: (state, val: number) => state.uiLightboxKey = val,
@@ -147,6 +162,7 @@ export default new Vuex.Store({
setUiLightboxThumbs: (state, val) => state.uiLightboxThumbs = val, setUiLightboxThumbs: (state, val) => state.uiLightboxThumbs = val,
setUiLightboxTypes: (state, val) => state.uiLightboxTypes = val, setUiLightboxTypes: (state, val) => state.uiLightboxTypes = val,
setUiLightboxCaptions: (state, val) => state.uiLightboxCaptions = val, setUiLightboxCaptions: (state, val) => state.uiLightboxCaptions = val,
setUiSqliteMode: (state, val) => state.uiSqliteMode = val,
setOptTheme: (state, val) => state.optTheme = val, setOptTheme: (state, val) => state.optTheme = val,
setOptDisplay: (state, val) => state.optDisplay = val, setOptDisplay: (state, val) => state.optDisplay = val,
@@ -172,6 +188,15 @@ export default new Vuex.Store({
setOptVidPreviewInterval: (state, val) => state.optVidPreviewInterval = val, setOptVidPreviewInterval: (state, val) => state.optVidPreviewInterval = val,
setOptSimpleLightbox: (state, val) => state.optSimpleLightbox = val, setOptSimpleLightbox: (state, val) => state.optSimpleLightbox = val,
setOptShowTagPickerFilter: (state, val) => state.optShowTagPickerFilter = val, setOptShowTagPickerFilter: (state, val) => state.optShowTagPickerFilter = val,
setOptAutoAnalyze: (state, val) => {
state.optAutoAnalyze = val
},
setOptMlRepositories: (state, val) => {
state.optMlRepositories = val
},
setOptMlDefaultModel: (state, val) => {
state.optMlDefaultModel = val
},
setOptLightboxLoadOnlyCurrent: (state, val) => state.optLightboxLoadOnlyCurrent = val, setOptLightboxLoadOnlyCurrent: (state, val) => state.optLightboxLoadOnlyCurrent = val,
setOptLightboxSlideDuration: (state, val) => state.optLightboxSlideDuration = val, setOptLightboxSlideDuration: (state, val) => state.optLightboxSlideDuration = val,
@@ -194,6 +219,8 @@ export default new Vuex.Store({
// noop // noop
}, },
setAuth0Token: (state, val) => state.auth0Token = val, setAuth0Token: (state, val) => state.auth0Token = val,
setNerModel: (state, val) => state.nerModel = val,
setEmbeddingsModel: (state, val) => state.embeddingsModel = val,
}, },
actions: { actions: {
setSist2Info: (store, val) => { setSist2Info: (store, val) => {
@@ -329,6 +356,7 @@ export default new Vuex.Store({
commit("_setUiShowLightbox", !state.uiShowLightbox); commit("_setUiShowLightbox", !state.uiShowLightbox);
}, },
clearResults({commit}) { clearResults({commit}) {
commit("setFirstQueryResult", null);
commit("setLastQueryResult", null); commit("setLastQueryResult", null);
commit("_setKeySequence", 0); commit("_setKeySequence", 0);
commit("_setUiShowLightbox", false); commit("_setUiShowLightbox", false);
@@ -350,6 +378,9 @@ export default new Vuex.Store({
}, },
modules: {}, modules: {},
getters: { getters: {
nerModel: (state) => state.nerModel,
embeddingsModel: (state) => state.embeddingsModel,
embedding: (state) => state.embedding,
seed: (state) => state.seed, seed: (state) => state.seed,
getPathText: (state) => state.pathText, getPathText: (state) => state.pathText,
indices: state => state.indices, indices: state => state.indices,
@@ -368,6 +399,7 @@ export default new Vuex.Store({
sizeMin: state => state.sizeMin, sizeMin: state => state.sizeMin,
sizeMax: state => state.sizeMax, sizeMax: state => state.sizeMax,
searchText: state => state.searchText, searchText: state => state.searchText,
embeddingText: state => state.embeddingText,
pathText: state => state.pathText, pathText: state => state.pathText,
fuzzy: state => state.fuzzy, fuzzy: state => state.fuzzy,
size: state => state.optSize, size: state => state.optSize,
@@ -380,7 +412,6 @@ export default new Vuex.Store({
return (state.lastQueryResults as unknown as EsResult).hits.hits.slice(-1)[0]; return (state.lastQueryResults as unknown as EsResult).hits.hits.slice(-1)[0];
}, },
uiTagHover: state => state.uiTagHover,
uiShowLightbox: state => state.uiShowLightbox, uiShowLightbox: state => state.uiShowLightbox,
uiLightboxSources: state => state.uiLightboxSources, uiLightboxSources: state => state.uiLightboxSources,
uiLightboxThumbs: state => state.uiLightboxThumbs, uiLightboxThumbs: state => state.uiLightboxThumbs,
@@ -388,6 +419,7 @@ export default new Vuex.Store({
uiLightboxTypes: state => state.uiLightboxTypes, uiLightboxTypes: state => state.uiLightboxTypes,
uiLightboxKey: state => state.uiLightboxKey, uiLightboxKey: state => state.uiLightboxKey,
uiLightboxSlide: state => state.uiLightboxSlide, uiLightboxSlide: state => state.uiLightboxSlide,
uiSqliteMode: state => state.uiSqliteMode,
optHideDuplicates: state => state.optHideDuplicates, optHideDuplicates: state => state.optHideDuplicates,
optLang: state => state.optLang, optLang: state => state.optLang,
@@ -416,5 +448,12 @@ export default new Vuex.Store({
optSimpleLightbox: state => state.optSimpleLightbox, optSimpleLightbox: state => state.optSimpleLightbox,
optShowTagPickerFilter: state => state.optShowTagPickerFilter, optShowTagPickerFilter: state => state.optShowTagPickerFilter,
optFeaturedFields: state => state.optFeaturedFields, optFeaturedFields: state => state.optFeaturedFields,
optMlRepositories: state => state.optMlRepositories,
mlRepositoryList: state => {
const repos = state.optMlRepositories.split("\n")
return repos[0] == "" ? [] : repos;
},
optMlDefaultModel: state => state.optMlDefaultModel,
optAutoAnalyze: state => state.optAutoAnalyze,
} }
}) })

View File

@@ -1,202 +1,221 @@
<template> <template>
<!-- <div :style="{width: `${$store.getters.optContainerWidth}px`}"--> <!-- <div :style="{width: `${$store.getters.optContainerWidth}px`}"-->
<div <div
v-if="!configLoading" v-if="!configLoading"
style="margin-left: auto; margin-right: auto;" class="container"> style="margin-left: auto; margin-right: auto;" class="container">
<b-card>
<b-card-title>
<GearIcon></GearIcon>
{{ $t("config") }}
</b-card-title>
<p>{{ $t("configDescription") }}</p>
<b-card-body>
<h4>{{ $t("displayOptions") }}</h4>
<b-card> <b-card>
<b-card-title>
<GearIcon></GearIcon>
{{ $t("config") }}
</b-card-title>
<p>{{ $t("configDescription") }}</p>
<label> <b-card-body>
<LanguageIcon/> <h4>{{ $t("displayOptions") }}</h4>
<span style="vertical-align: middle">&nbsp;{{ $t("opt.lang") }}</span></label>
<b-form-select :options="langOptions" :value="optLang" @input="setOptLang"></b-form-select>
<label>{{ $t("opt.theme") }}</label> <b-card>
<b-form-select :options="themeOptions" :value="optTheme" @input="setOptTheme"></b-form-select>
<label>{{ $t("opt.displayMode") }}</label> <label>
<b-form-select :options="displayModeOptions" :value="optDisplay" @input="setOptDisplay"></b-form-select> <LanguageIcon/>
<span style="vertical-align: middle">&nbsp;{{ $t("opt.lang") }}</span></label>
<b-form-select :options="langOptions" :value="optLang" @input="setOptLang"></b-form-select>
<label>{{ $t("opt.columns") }}</label> <label>{{ $t("opt.theme") }}</label>
<b-form-select :options="columnsOptions" :value="optColumns" @input="setOptColumns"></b-form-select> <b-form-select :options="themeOptions" :value="optTheme" @input="setOptTheme"></b-form-select>
<div style="height: 10px"></div> <label>{{ $t("opt.displayMode") }}</label>
<b-form-select :options="displayModeOptions" :value="optDisplay"
@input="setOptDisplay"></b-form-select>
<b-form-checkbox :checked="optLightboxLoadOnlyCurrent" @input="setOptLightboxLoadOnlyCurrent"> <label>{{ $t("opt.columns") }}</label>
{{ $t("opt.lightboxLoadOnlyCurrent") }} <b-form-select :options="columnsOptions" :value="optColumns" @input="setOptColumns"></b-form-select>
</b-form-checkbox>
<b-form-checkbox :checked="optHideLegacy" @input="setOptHideLegacy"> <div style="height: 10px"></div>
{{ $t("opt.hideLegacy") }}
</b-form-checkbox>
<b-form-checkbox :checked="optUpdateMimeMap" @input="setOptUpdateMimeMap"> <b-form-checkbox :checked="optLightboxLoadOnlyCurrent" @input="setOptLightboxLoadOnlyCurrent">
{{ $t("opt.updateMimeMap") }} {{ $t("opt.lightboxLoadOnlyCurrent") }}
</b-form-checkbox> </b-form-checkbox>
<b-form-checkbox :checked="optUseDatePicker" @input="setOptUseDatePicker"> <b-form-checkbox :disabled="uiSqliteMode" :checked="optHideLegacy" @input="setOptHideLegacy">
{{ $t("opt.useDatePicker") }} {{ $t("opt.hideLegacy") }}
</b-form-checkbox> </b-form-checkbox>
<b-form-checkbox :checked="optSimpleLightbox" @input="setOptSimpleLightbox">{{ <b-form-checkbox :disabled="uiSqliteMode" :checked="optUpdateMimeMap" @input="setOptUpdateMimeMap">
$t("opt.simpleLightbox") {{ $t("opt.updateMimeMap") }}
}} </b-form-checkbox>
</b-form-checkbox>
<b-form-checkbox :checked="optShowTagPickerFilter" @input="setOptShowTagPickerFilter">{{ <b-form-checkbox :checked="optUseDatePicker" @input="setOptUseDatePicker">
$t("opt.showTagPickerFilter") {{ $t("opt.useDatePicker") }}
}} </b-form-checkbox>
</b-form-checkbox>
<br/> <b-form-checkbox :checked="optSimpleLightbox" @input="setOptSimpleLightbox">{{
<label>{{ $t("opt.featuredFields") }}</label> $t("opt.simpleLightbox")
}}
</b-form-checkbox>
<br> <b-form-checkbox :checked="optShowTagPickerFilter" @input="setOptShowTagPickerFilter">{{
<b-button v-b-toggle.collapse-1 variant="secondary" class="dropdown-toggle">{{ $t("opt.showTagPickerFilter")
$t("opt.featuredFieldsList") }}
}} </b-form-checkbox>
</b-button>
<b-collapse id="collapse-1" class="mt-2">
<ul>
<li><code>doc.checksum</code></li>
<li><code>doc.path</code></li>
<li><code>doc.mime</code></li>
<li><code>doc.videoc</code></li>
<li><code>doc.audioc</code></li>
<li><code>doc.pages</code></li>
<li><code>doc.mtime</code></li>
<li><code>doc.font_name</code></li>
<li><code>doc.album</code></li>
<li><code>doc.artist</code></li>
<li><code>doc.title</code></li>
<li><code>doc.genre</code></li>
<li><code>doc.album_artist</code></li>
<li><code>doc.exif_make</code></li>
<li><code>doc.exif_model</code></li>
<li><code>doc.exif_software</code></li>
<li><code>doc.exif_exposure_time</code></li>
<li><code>doc.exif_fnumber</code></li>
<li><code>doc.exif_iso_speed_ratings</code></li>
<li><code>doc.exif_focal_length</code></li>
<li><code>doc.exif_user_comment</code></li>
<li><code>doc.exif_user_comment</code></li>
<li><code>doc.exif_gps_longitude_ref</code></li>
<li><code>doc.exif_gps_longitude_dms</code></li>
<li><code>doc.exif_gps_longitude_dec</code></li>
<li><code>doc.exif_gps_latitude_ref</code></li>
<li><code>doc.exif_gps_latitude_dec</code></li>
<li><code>humanDate()</code></li>
<li><code>humanFileSize()</code></li>
</ul>
<p>{{ $t("forExample") }}</p> <br/>
<label>{{ $t("opt.featuredFields") }}</label>
<ul> <br>
<li> <b-button v-b-toggle.collapse-1 variant="secondary" class="dropdown-toggle">{{
<code>&lt;b&gt;${humanDate(doc.mtime)}&lt;/b&gt; ${doc.videoc || ''}</code> $t("opt.featuredFieldsList")
</li> }}
<li> </b-button>
<code>${doc.pages ? (doc.pages + ' pages') : ''}</code> <b-collapse id="collapse-1" class="mt-2">
</li> <ul>
</ul> <li><code>doc.checksum</code></li>
</b-collapse> <li><code>doc.path</code></li>
<br/> <li><code>doc.mime</code></li>
<br/> <li><code>doc.videoc</code></li>
<b-textarea rows="3" :value="optFeaturedFields" @input="setOptFeaturedFields"></b-textarea> <li><code>doc.audioc</code></li>
<li><code>doc.pages</code></li>
<li><code>doc.mtime</code></li>
<li><code>doc.font_name</code></li>
<li><code>doc.album</code></li>
<li><code>doc.artist</code></li>
<li><code>doc.title</code></li>
<li><code>doc.genre</code></li>
<li><code>doc.album_artist</code></li>
<li><code>doc.exif_make</code></li>
<li><code>doc.exif_model</code></li>
<li><code>doc.exif_software</code></li>
<li><code>doc.exif_exposure_time</code></li>
<li><code>doc.exif_fnumber</code></li>
<li><code>doc.exif_iso_speed_ratings</code></li>
<li><code>doc.exif_focal_length</code></li>
<li><code>doc.exif_user_comment</code></li>
<li><code>doc.exif_user_comment</code></li>
<li><code>doc.exif_gps_longitude_ref</code></li>
<li><code>doc.exif_gps_longitude_dms</code></li>
<li><code>doc.exif_gps_longitude_dec</code></li>
<li><code>doc.exif_gps_latitude_ref</code></li>
<li><code>doc.exif_gps_latitude_dec</code></li>
<li><code>humanDate()</code></li>
<li><code>humanFileSize()</code></li>
</ul>
<p>{{ $t("forExample") }}</p>
<ul>
<li>
<code>&lt;b&gt;${humanDate(doc.mtime)}&lt;/b&gt; ${doc.videoc || ''}</code>
</li>
<li>
<code>${doc.pages ? (doc.pages + ' pages') : ''}</code>
</li>
</ul>
</b-collapse>
<br/>
<br/>
<b-textarea rows="3" :value="optFeaturedFields" @input="setOptFeaturedFields"></b-textarea>
</b-card>
<br/>
<h4>{{ $t("searchOptions") }}</h4>
<b-card>
<b-form-checkbox :checked="optHideDuplicates" @input="setOptHideDuplicates">{{
$t("opt.hideDuplicates")
}}
</b-form-checkbox>
<b-form-checkbox :checked="optHighlight" @input="setOptHighlight">{{
$t("opt.highlight")
}}
</b-form-checkbox>
<b-form-checkbox :checked="optTagOrOperator" @input="setOptTagOrOperator">{{
$t("opt.tagOrOperator")
}}
</b-form-checkbox>
<b-form-checkbox :disabled="uiSqliteMode" :checked="optFuzzy" @input="setOptFuzzy">
{{ $t("opt.fuzzy") }}
</b-form-checkbox>
<b-form-checkbox :disabled="uiSqliteMode" :checked="optSearchInPath" @input="setOptSearchInPath">{{
$t("opt.searchInPath")
}}
</b-form-checkbox>
<b-form-checkbox :checked="optSuggestPath" @input="setOptSuggestPath">{{
$t("opt.suggestPath")
}}
</b-form-checkbox>
<br/>
<label>{{ $t("opt.fragmentSize") }}</label>
<b-form-input :value="optFragmentSize" step="10" type="number" min="0"
@input="setOptFragmentSize"></b-form-input>
<label>{{ $t("opt.resultSize") }}</label>
<b-form-input :value="optResultSize" type="number" min="10"
@input="setOptResultSize"></b-form-input>
<label :class="{'text-muted': uiSqliteMode}">{{ $t("opt.queryMode") }}</label>
<b-form-select :disabled="uiSqliteMode" :options="queryModeOptions" :value="optQueryMode"
@input="setOptQueryMode"></b-form-select>
<label>{{ $t("opt.slideDuration") }}</label>
<b-form-input :value="optLightboxSlideDuration" type="number" min="1"
@input="setOptLightboxSlideDuration"></b-form-input>
<label>{{ $t("opt.vidPreviewInterval") }}</label>
<b-form-input :value="optVidPreviewInterval" type="number" min="50"
@input="setOptVidPreviewInterval"></b-form-input>
</b-card>
<h4 class="mt-3">{{ $t("mlOptions") }}</h4>
<b-card>
<label>{{ $t("opt.mlRepositories") }}</label>
<b-textarea rows="3" :value="optMlRepositories" @input="setOptMlRepositories"></b-textarea>
<br>
<b-form-checkbox :checked="optAutoAnalyze" @input="setOptAutoAnalyze">{{
$t("opt.autoAnalyze")
}}
</b-form-checkbox>
</b-card>
<h4 class="mt-3">{{ $t("treemapOptions") }}</h4>
<b-card>
<label>{{ $t("opt.treemapType") }}</label>
<b-form-select :value="optTreemapType" :options="treemapTypeOptions"
@input="setOptTreemapType"></b-form-select>
<label>{{ $t("opt.treemapTiling") }}</label>
<b-form-select :value="optTreemapTiling" :options="treemapTilingOptions"
@input="setOptTreemapTiling"></b-form-select>
<label>{{ $t("opt.treemapColorGroupingDepth") }}</label>
<b-form-input :value="optTreemapColorGroupingDepth" type="number" min="1"
@input="setOptTreemapColorGroupingDepth"></b-form-input>
<label>{{ $t("opt.treemapSize") }}</label>
<b-form-select :value="optTreemapSize" :options="treemapSizeOptions"
@input="setOptTreemapSize"></b-form-select>
<template v-if="$store.getters.optTreemapSize === 'custom'">
<!-- TODO Width/Height input -->
<b-form-input type="number" min="0" step="10"></b-form-input>
<b-form-input type="number" min="0" step="10"></b-form-input>
</template>
<label>{{ $t("opt.treemapColor") }}</label>
<b-form-select :value="optTreemapColor" :options="treemapColorOptions"
@input="setOptTreemapColor"></b-form-select>
</b-card>
<b-button variant="danger" class="mt-4" @click="onResetClick()">{{ $t("configReset") }}</b-button>
</b-card-body>
</b-card> </b-card>
<br/> <b-card v-if="loading" class="mt-4">
<h4>{{ $t("searchOptions") }}</h4> <Preloader></Preloader>
<b-card>
<b-form-checkbox :checked="optHideDuplicates" @input="setOptHideDuplicates">{{
$t("opt.hideDuplicates")
}}
</b-form-checkbox>
<b-form-checkbox :checked="optHighlight" @input="setOptHighlight">{{ $t("opt.highlight") }}</b-form-checkbox>
<b-form-checkbox :checked="optTagOrOperator" @input="setOptTagOrOperator">{{
$t("opt.tagOrOperator")
}}
</b-form-checkbox>
<b-form-checkbox :checked="optFuzzy" @input="setOptFuzzy">{{ $t("opt.fuzzy") }}</b-form-checkbox>
<b-form-checkbox :checked="optSearchInPath" @input="setOptSearchInPath">{{
$t("opt.searchInPath")
}}
</b-form-checkbox>
<b-form-checkbox :checked="optSuggestPath" @input="setOptSuggestPath">{{
$t("opt.suggestPath")
}}
</b-form-checkbox>
<br/>
<label>{{ $t("opt.fragmentSize") }}</label>
<b-form-input :value="optFragmentSize" step="10" type="number" min="0"
@input="setOptFragmentSize"></b-form-input>
<label>{{ $t("opt.resultSize") }}</label>
<b-form-input :value="optResultSize" type="number" min="10"
@input="setOptResultSize"></b-form-input>
<label>{{ $t("opt.queryMode") }}</label>
<b-form-select :options="queryModeOptions" :value="optQueryMode" @input="setOptQueryMode"></b-form-select>
<label>{{ $t("opt.slideDuration") }}</label>
<b-form-input :value="optLightboxSlideDuration" type="number" min="1"
@input="setOptLightboxSlideDuration"></b-form-input>
<label>{{ $t("opt.vidPreviewInterval") }}</label>
<b-form-input :value="optVidPreviewInterval" type="number" min="50"
@input="setOptVidPreviewInterval"></b-form-input>
</b-card> </b-card>
<DebugInfo v-else></DebugInfo>
<h4 class="mt-3">{{ $t("treemapOptions") }}</h4> </div>
<b-card>
<label>{{ $t("opt.treemapType") }}</label>
<b-form-select :value="optTreemapType" :options="treemapTypeOptions"
@input="setOptTreemapType"></b-form-select>
<label>{{ $t("opt.treemapTiling") }}</label>
<b-form-select :value="optTreemapTiling" :options="treemapTilingOptions"
@input="setOptTreemapTiling"></b-form-select>
<label>{{ $t("opt.treemapColorGroupingDepth") }}</label>
<b-form-input :value="optTreemapColorGroupingDepth" type="number" min="1"
@input="setOptTreemapColorGroupingDepth"></b-form-input>
<label>{{ $t("opt.treemapSize") }}</label>
<b-form-select :value="optTreemapSize" :options="treemapSizeOptions"
@input="setOptTreemapSize"></b-form-select>
<template v-if="$store.getters.optTreemapSize === 'custom'">
<!-- TODO Width/Height input -->
<b-form-input type="number" min="0" step="10"></b-form-input>
<b-form-input type="number" min="0" step="10"></b-form-input>
</template>
<label>{{ $t("opt.treemapColor") }}</label>
<b-form-select :value="optTreemapColor" :options="treemapColorOptions"
@input="setOptTreemapColor"></b-form-select>
</b-card>
<b-button variant="danger" class="mt-4" @click="onResetClick()">{{ $t("configReset") }}</b-button>
</b-card-body>
</b-card>
<b-card v-if="loading" class="mt-4">
<Preloader></Preloader>
</b-card>
<DebugInfo v-else></DebugInfo>
</div>
</template> </template>
<script> <script>
@@ -208,164 +227,170 @@ import GearIcon from "@/components/icons/GearIcon.vue";
import LanguageIcon from "@/components/icons/LanguageIcon"; import LanguageIcon from "@/components/icons/LanguageIcon";
export default { export default {
components: {LanguageIcon, GearIcon, DebugInfo, Preloader}, components: {LanguageIcon, GearIcon, DebugInfo, Preloader},
data() { data() {
return { return {
loading: false, loading: false,
configLoading: false, configLoading: false,
langOptions: [ langOptions: [
{value: "en", text: this.$t("lang.en")}, {value: "en", text: this.$t("lang.en")},
{value: "fr", text: this.$t("lang.fr")}, {value: "fr", text: this.$t("lang.fr")},
{value: "zh-CN", text: this.$t("lang.zh-CN")}, {value: "zh-CN", text: this.$t("lang.zh-CN")},
{value: "de", text: this.$t("lang.de")}, {value: "de", text: this.$t("lang.de")},
], {value: "pl", text: this.$t("lang.pl")},
queryModeOptions: [ ],
{value: "simple", text: this.$t("queryMode.simple")}, queryModeOptions: [
{value: "advanced", text: this.$t("queryMode.advanced")} {value: "simple", text: this.$t("queryMode.simple")},
], {value: "advanced", text: this.$t("queryMode.advanced")}
displayModeOptions: [ ],
{value: "grid", text: this.$t("displayMode.grid")}, displayModeOptions: [
{value: "list", text: this.$t("displayMode.list")} {value: "grid", text: this.$t("displayMode.grid")},
], {value: "list", text: this.$t("displayMode.list")}
columnsOptions: [ ],
{value: "auto", text: this.$t("columns.auto")}, columnsOptions: [
{value: 1, text: "1"}, {value: "auto", text: this.$t("columns.auto")},
{value: 2, text: "2"}, {value: 1, text: "1"},
{value: 3, text: "3"}, {value: 2, text: "2"},
{value: 4, text: "4"}, {value: 3, text: "3"},
{value: 5, text: "5"}, {value: 4, text: "4"},
{value: 6, text: "6"}, {value: 5, text: "5"},
{value: 7, text: "7"}, {value: 6, text: "6"},
{value: 8, text: "8"}, {value: 7, text: "7"},
{value: 9, text: "9"}, {value: 8, text: "8"},
{value: 10, text: "10"}, {value: 9, text: "9"},
{value: 11, text: "11"}, {value: 10, text: "10"},
{value: 12, text: "12"}, {value: 11, text: "11"},
], {value: 12, text: "12"},
treemapTypeOptions: [ ],
{value: "cascaded", text: this.$t("treemapType.cascaded")}, treemapTypeOptions: [
{value: "flat", text: this.$t("treemapType.flat")} {value: "cascaded", text: this.$t("treemapType.cascaded")},
], {value: "flat", text: this.$t("treemapType.flat")}
treemapTilingOptions: [ ],
{value: "binary", text: this.$t("treemapTiling.binary")}, treemapTilingOptions: [
{value: "squarify", text: this.$t("treemapTiling.squarify")}, {value: "binary", text: this.$t("treemapTiling.binary")},
{value: "slice", text: this.$t("treemapTiling.slice")}, {value: "squarify", text: this.$t("treemapTiling.squarify")},
{value: "dice", text: this.$t("treemapTiling.dice")}, {value: "slice", text: this.$t("treemapTiling.slice")},
{value: "sliceDice", text: this.$t("treemapTiling.sliceDice")}, {value: "dice", text: this.$t("treemapTiling.dice")},
], {value: "sliceDice", text: this.$t("treemapTiling.sliceDice")},
treemapSizeOptions: [ ],
{value: "small", text: this.$t("treemapSize.small")}, treemapSizeOptions: [
{value: "medium", text: this.$t("treemapSize.medium")}, {value: "small", text: this.$t("treemapSize.small")},
{value: "large", text: this.$t("treemapSize.large")}, {value: "medium", text: this.$t("treemapSize.medium")},
{value: "x-large", text: this.$t("treemapSize.xLarge")}, {value: "large", text: this.$t("treemapSize.large")},
{value: "xx-large", text: this.$t("treemapSize.xxLarge")}, {value: "x-large", text: this.$t("treemapSize.xLarge")},
// {value: "custom", text: this.$t("treemapSize.custom")}, {value: "xx-large", text: this.$t("treemapSize.xxLarge")},
], // {value: "custom", text: this.$t("treemapSize.custom")},
treemapColorOptions: [ ],
{value: "PuBuGn", text: "Purple-Blue-Green"}, treemapColorOptions: [
{value: "PuRd", text: "Purple-Red"}, {value: "PuBuGn", text: "Purple-Blue-Green"},
{value: "PuBu", text: "Purple-Blue"}, {value: "PuRd", text: "Purple-Red"},
{value: "YlOrBr", text: "Yellow-Orange-Brown"}, {value: "PuBu", text: "Purple-Blue"},
{value: "YlOrRd", text: "Yellow-Orange-Red"}, {value: "YlOrBr", text: "Yellow-Orange-Brown"},
{value: "YlGn", text: "Yellow-Green"}, {value: "YlOrRd", text: "Yellow-Orange-Red"},
{value: "YlGnBu", text: "Yellow-Green-Blue"}, {value: "YlGn", text: "Yellow-Green"},
{value: "Plasma", text: "Plasma"}, {value: "YlGnBu", text: "Yellow-Green-Blue"},
{value: "Magma", text: "Magma"}, {value: "Plasma", text: "Plasma"},
{value: "Inferno", text: "Inferno"}, {value: "Magma", text: "Magma"},
{value: "Viridis", text: "Viridis"}, {value: "Inferno", text: "Inferno"},
{value: "Turbo", text: "Turbo"}, {value: "Viridis", text: "Viridis"},
], {value: "Turbo", text: "Turbo"},
themeOptions: [ ],
{value: "light", text: this.$t("theme.light")}, themeOptions: [
{value: "black", text: this.$t("theme.black")} {value: "light", text: this.$t("theme.light")},
] {value: "black", text: this.$t("theme.black")}
]
} }
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
"optTheme", "uiSqliteMode",
"optDisplay", "optTheme",
"optColumns", "optDisplay",
"optHighlight", "optColumns",
"optFuzzy", "optHighlight",
"optSearchInPath", "optFuzzy",
"optSuggestPath", "optSearchInPath",
"optFragmentSize", "optSuggestPath",
"optQueryMode", "optFragmentSize",
"optTreemapType", "optQueryMode",
"optTreemapTiling", "optTreemapType",
"optTreemapColorGroupingDepth", "optTreemapTiling",
"optTreemapColor", "optTreemapColorGroupingDepth",
"optTreemapSize", "optTreemapColor",
"optLightboxLoadOnlyCurrent", "optTreemapSize",
"optLightboxSlideDuration", "optLightboxLoadOnlyCurrent",
"optResultSize", "optLightboxSlideDuration",
"optTagOrOperator", "optResultSize",
"optLang", "optTagOrOperator",
"optHideDuplicates", "optLang",
"optHideLegacy", "optHideDuplicates",
"optUpdateMimeMap", "optHideLegacy",
"optUseDatePicker", "optUpdateMimeMap",
"optVidPreviewInterval", "optUseDatePicker",
"optSimpleLightbox", "optVidPreviewInterval",
"optShowTagPickerFilter", "optSimpleLightbox",
"optFeaturedFields", "optShowTagPickerFilter",
]), "optFeaturedFields",
clientWidth() { "optMlRepositories",
return window.innerWidth; "optAutoAnalyze",
} ]),
}, clientWidth() {
mounted() { return window.innerWidth;
this.$store.subscribe((mutation) => { }
if (mutation.type.startsWith("setOpt")) { },
this.$store.dispatch("updateConfiguration"); mounted() {
} this.$store.subscribe((mutation) => {
}); if (mutation.type.startsWith("setOpt")) {
}, this.$store.dispatch("updateConfiguration");
methods: { }
...mapActions({ });
setSist2Info: "setSist2Info", },
}), methods: {
...mapMutations([ ...mapActions({
"setOptTheme", setSist2Info: "setSist2Info",
"setOptDisplay", }),
"setOptColumns", ...mapMutations([
"setOptHighlight", "setOptTheme",
"setOptFuzzy", "setOptDisplay",
"setOptSearchInPath", "setOptColumns",
"setOptSuggestPath", "setOptHighlight",
"setOptFragmentSize", "setOptFuzzy",
"setOptQueryMode", "setOptSearchInPath",
"setOptTreemapType", "setOptSuggestPath",
"setOptTreemapTiling", "setOptFragmentSize",
"setOptTreemapColorGroupingDepth", "setOptQueryMode",
"setOptTreemapColor", "setOptTreemapType",
"setOptTreemapSize", "setOptTreemapTiling",
"setOptLightboxLoadOnlyCurrent", "setOptTreemapColorGroupingDepth",
"setOptLightboxSlideDuration", "setOptTreemapColor",
"setOptResultSize", "setOptTreemapSize",
"setOptTagOrOperator", "setOptLightboxLoadOnlyCurrent",
"setOptLang", "setOptLightboxSlideDuration",
"setOptHideDuplicates", "setOptResultSize",
"setOptHideLegacy", "setOptTagOrOperator",
"setOptUpdateMimeMap", "setOptLang",
"setOptUseDatePicker", "setOptHideDuplicates",
"setOptVidPreviewInterval", "setOptHideLegacy",
"setOptSimpleLightbox", "setOptUpdateMimeMap",
"setOptShowTagPickerFilter", "setOptUseDatePicker",
"setOptFeaturedFields", "setOptVidPreviewInterval",
]), "setOptSimpleLightbox",
onResetClick() { "setOptShowTagPickerFilter",
localStorage.removeItem("sist2_configuration"); "setOptFeaturedFields",
window.location.reload(); "setOptMlRepositories",
} "setOptAutoAnalyze",
}, ]),
onResetClick() {
localStorage.removeItem("sist2_configuration");
window.location.reload();
}
},
} }
</script> </script>
<style> <style>
.shrink { .shrink {
flex-grow: inherit; flex-grow: inherit;
} }
</style> </style>

Some files were not shown because too many files have changed in this diff Show More