mirror of
https://github.com/simon987/sist2.git
synced 2025-04-19 18:26:43 +00:00
Compare commits
No commits in common. "master" and "3.1.4" have entirely different histories.
9
.devcontainer/Dockerfile
Normal file
9
.devcontainer/Dockerfile
Normal 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
|
||||||
|
|
16
.devcontainer/devcontainer.json
Normal file
16
.devcontainer/devcontainer.json
Normal 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/"
|
||||||
|
}
|
8
.devcontainer/docker-compose.yml
Normal file
8
.devcontainer/docker-compose.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
sist2-dev:
|
||||||
|
build: .
|
||||||
|
command: sleep infinity
|
||||||
|
volumes:
|
||||||
|
- ../:/app
|
@ -38,6 +38,4 @@ build/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
sist2-vue/dist
|
sist2-vue/dist
|
||||||
sist2-admin/frontend/dist
|
sist2-admin/frontend/dist
|
||||||
*.fts
|
*.fts
|
||||||
.git
|
|
||||||
third-party/libscan/third-party/ext_*/*
|
|
88
.drone.yml
88
.drone.yml
@ -7,36 +7,11 @@ platform:
|
|||||||
arch: amd64
|
arch: amd64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: submodules
|
|
||||||
image: alpine/git
|
|
||||||
commands:
|
|
||||||
- git submodule update --init --recursive
|
|
||||||
- name: docker
|
|
||||||
image: plugins/docker
|
|
||||||
depends_on:
|
|
||||||
- submodules
|
|
||||||
settings:
|
|
||||||
username:
|
|
||||||
from_secret: DOCKER_USER
|
|
||||||
password:
|
|
||||||
from_secret: DOCKER_PASSWORD
|
|
||||||
repo: sist2app/sist2
|
|
||||||
context: ./
|
|
||||||
dockerfile: ./Dockerfile
|
|
||||||
auto_tag: true
|
|
||||||
auto_tag_suffix: x64-linux
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- tag
|
|
||||||
- name: build
|
- name: build
|
||||||
image: sist2app/sist2-build
|
image: simon987/sist2-build
|
||||||
depends_on:
|
|
||||||
- submodules
|
|
||||||
commands:
|
commands:
|
||||||
- ./scripts/build.sh
|
- ./scripts/build.sh
|
||||||
- name: scp files
|
- name: scp files
|
||||||
depends_on:
|
|
||||||
- build
|
|
||||||
image: appleboy/drone-scp
|
image: appleboy/drone-scp
|
||||||
settings:
|
settings:
|
||||||
host:
|
host:
|
||||||
@ -47,11 +22,26 @@ steps:
|
|||||||
from_secret: SSH_USER
|
from_secret: SSH_USER
|
||||||
key:
|
key:
|
||||||
from_secret: SSH_KEY
|
from_secret: SSH_KEY
|
||||||
target: ~/files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/
|
target: /files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/
|
||||||
source:
|
source:
|
||||||
- ./VERSION
|
- ./VERSION
|
||||||
- ./sist2-x64-linux
|
- ./sist2-x64-linux
|
||||||
- ./sist2-x64-linux-debug
|
- ./sist2-x64-linux-debug
|
||||||
|
- name: docker
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: DOCKER_USER
|
||||||
|
password:
|
||||||
|
from_secret: DOCKER_PASSWORD
|
||||||
|
repo: simon987/sist2
|
||||||
|
context: ./
|
||||||
|
dockerfile: ./Dockerfile
|
||||||
|
auto_tag: true
|
||||||
|
auto_tag_suffix: x64-linux
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
@ -62,36 +52,11 @@ platform:
|
|||||||
arch: arm64
|
arch: arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: submodules
|
|
||||||
image: alpine/git
|
|
||||||
commands:
|
|
||||||
- git submodule update --init --recursive
|
|
||||||
- name: docker
|
|
||||||
image: plugins/docker
|
|
||||||
depends_on:
|
|
||||||
- submodules
|
|
||||||
settings:
|
|
||||||
username:
|
|
||||||
from_secret: DOCKER_USER
|
|
||||||
password:
|
|
||||||
from_secret: DOCKER_PASSWORD
|
|
||||||
repo: sist2app/sist2
|
|
||||||
context: ./
|
|
||||||
dockerfile: ./Dockerfile.arm64
|
|
||||||
auto_tag: true
|
|
||||||
auto_tag_suffix: arm64-linux
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- tag
|
|
||||||
- name: build
|
- name: build
|
||||||
image: sist2app/sist2-build-arm64
|
image: simon987/sist2-build-arm64
|
||||||
depends_on:
|
|
||||||
- submodules
|
|
||||||
commands:
|
commands:
|
||||||
- ./scripts/build_arm64.sh
|
- ./scripts/build_arm64.sh
|
||||||
- name: scp files
|
- name: scp files
|
||||||
depends_on:
|
|
||||||
- build
|
|
||||||
image: appleboy/drone-scp
|
image: appleboy/drone-scp
|
||||||
settings:
|
settings:
|
||||||
host:
|
host:
|
||||||
@ -102,7 +67,22 @@ steps:
|
|||||||
from_secret: SSH_USER
|
from_secret: SSH_USER
|
||||||
key:
|
key:
|
||||||
from_secret: SSH_KEY
|
from_secret: SSH_KEY
|
||||||
target: ~/files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/arm_${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/
|
target: /files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/arm_${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/
|
||||||
source:
|
source:
|
||||||
- ./sist2-arm64-linux
|
- ./sist2-arm64-linux
|
||||||
- ./sist2-arm64-linux-debug
|
- ./sist2-arm64-linux-debug
|
||||||
|
- name: docker
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: DOCKER_USER
|
||||||
|
password:
|
||||||
|
from_secret: DOCKER_PASSWORD
|
||||||
|
repo: simon987/sist2
|
||||||
|
context: ./
|
||||||
|
dockerfile: ./Dockerfile.arm64
|
||||||
|
auto_tag: true
|
||||||
|
auto_tag_suffix: arm64-linux
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,7 +3,6 @@ thumbs
|
|||||||
*.cbp
|
*.cbp
|
||||||
CMakeCache.txt
|
CMakeCache.txt
|
||||||
CMakeFiles
|
CMakeFiles
|
||||||
cmake-build-default-event-trace
|
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
cmake_install.cmake
|
cmake_install.cmake
|
||||||
Makefile
|
Makefile
|
||||||
|
@ -53,6 +53,7 @@ add_executable(
|
|||||||
src/types.h
|
src/types.h
|
||||||
src/log.c src/log.h
|
src/log.c src/log.h
|
||||||
src/cli.c src/cli.h
|
src/cli.c src/cli.h
|
||||||
|
src/parsing/sidecar.c src/parsing/sidecar.h
|
||||||
src/database/database.c src/database/database.h
|
src/database/database.c src/database/database.h
|
||||||
src/parsing/fs_util.h
|
src/parsing/fs_util.h
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ add_executable(
|
|||||||
src/database/database_schema.c
|
src/database/database_schema.c
|
||||||
src/database/database_fts.c
|
src/database/database_fts.c
|
||||||
src/web/web_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/)
|
||||||
@ -75,7 +76,6 @@ 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(
|
||||||
@ -147,7 +147,6 @@ add_dependencies(
|
|||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
sist2
|
sist2
|
||||||
|
|
||||||
m
|
|
||||||
z
|
z
|
||||||
argparse
|
argparse
|
||||||
unofficial::mongoose::mongoose
|
unofficial::mongoose::mongoose
|
||||||
@ -159,7 +158,6 @@ target_link_libraries(
|
|||||||
|
|
||||||
${MAGIC_LIB}
|
${MAGIC_LIB}
|
||||||
unofficial::sqlite3::sqlite3
|
unofficial::sqlite3::sqlite3
|
||||||
OpenBLAS::OpenBLAS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(
|
add_custom_target(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
FROM sist2app/sist2-build as build
|
FROM simon987/sist2-build as build
|
||||||
|
MAINTAINER simon987 <me@simon987.net>
|
||||||
|
|
||||||
WORKDIR /build/
|
WORKDIR /build/
|
||||||
|
|
||||||
@ -47,6 +48,5 @@ COPY --from=build /build/build/sist2 /root/sist2
|
|||||||
# sist2-admin
|
# sist2-admin
|
||||||
WORKDIR /root/sist2-admin
|
WORKDIR /root/sist2-admin
|
||||||
COPY sist2-admin/requirements.txt /root/sist2-admin/
|
COPY sist2-admin/requirements.txt /root/sist2-admin/
|
||||||
RUN ln /usr/bin/python3 /usr/bin/python
|
RUN python3 -m pip install --no-cache -r /root/sist2-admin/requirements.txt
|
||||||
RUN python -m pip install --no-cache -r /root/sist2-admin/requirements.txt
|
|
||||||
COPY --from=build /build/sist2-admin/ /root/sist2-admin/
|
COPY --from=build /build/sist2-admin/ /root/sist2-admin/
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
FROM sist2app/sist2-build-arm64 as build
|
FROM simon987/sist2-build-arm64 as build
|
||||||
|
MAINTAINER simon987 <me@simon987.net>
|
||||||
|
|
||||||
WORKDIR /build/
|
WORKDIR /build/
|
||||||
|
|
||||||
|
84
README.md
84
README.md
@ -1,11 +1,9 @@
|
|||||||

|

|
||||||
[](https://www.codefactor.io/repository/github/sist2app/sist2)
|
[](https://www.codefactor.io/repository/github/simon987/sist2)
|
||||||
[](https://files.simon987.net/.gate/sist2/simon987_sist2/)
|
[](https://files.simon987.net/.gate/sist2/simon987_sist2/)
|
||||||
|
|
||||||
**Demo**: [sist2.simon987.net](https://sist2.simon987.net/)
|
**Demo**: [sist2.simon987.net](https://sist2.simon987.net/)
|
||||||
|
|
||||||
**Community URL:** [Discord](https://discord.gg/2PEjDy3Rfs)
|
|
||||||
|
|
||||||
# sist2
|
# sist2
|
||||||
|
|
||||||
sist2 (Simple incremental search tool)
|
sist2 (Simple incremental search tool)
|
||||||
@ -38,32 +36,26 @@ sist2 (Simple incremental search tool)
|
|||||||
### Using Docker Compose *(Windows/Linux/Mac)*
|
### Using Docker Compose *(Windows/Linux/Mac)*
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
image: elasticsearch:7.17.9
|
image: elasticsearch:7.17.9
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
|
||||||
# This directory must have 1000:1000 permissions (or update PUID & PGID below)
|
|
||||||
- /data/sist2-es-data/:/usr/share/elasticsearch/data
|
|
||||||
environment:
|
environment:
|
||||||
- "discovery.type=single-node"
|
- "discovery.type=single-node"
|
||||||
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||||
- "PUID=1000"
|
|
||||||
- "PGID=1000"
|
|
||||||
sist2-admin:
|
sist2-admin:
|
||||||
image: sist2app/sist2:x64-linux
|
image: simon987/sist2:3.1.0-x64-linux
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- /data/sist2-admin-data/:/sist2-admin/
|
- ./sist2-admin-data/:/sist2-admin/
|
||||||
- /<path to index>/:/host
|
- /:/host
|
||||||
ports:
|
ports:
|
||||||
- 4090:4090
|
- 4090:4090 # sist2
|
||||||
# NOTE: Don't expose this port publicly!
|
- 8080:8080 # sist2-admin
|
||||||
- 8080:8080
|
|
||||||
working_dir: /root/sist2-admin/
|
working_dir: /root/sist2-admin/
|
||||||
entrypoint: python3
|
entrypoint: python3 /root/sist2-admin/sist2_admin/app.py
|
||||||
command:
|
|
||||||
- /root/sist2-admin/sist2_admin/app.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Navigate to http://localhost:8080/ to configure sist2-admin.
|
Navigate to http://localhost:8080/ to configure sist2-admin.
|
||||||
@ -79,7 +71,7 @@ Navigate to http://localhost:8080/ to configure sist2-admin.
|
|||||||
```
|
```
|
||||||
* **SQLite**: No installation required
|
* **SQLite**: No installation required
|
||||||
|
|
||||||
2. Download the [latest sist2 release](https://github.com/sist2app/sist2/releases).
|
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`.
|
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.
|
3. See [usage guide](docs/USAGE.md) for command line usage.
|
||||||
|
|
||||||
@ -88,30 +80,28 @@ Example usage:
|
|||||||
1. Scan a directory: `sist2 scan ~/Documents --output ./documents.sist2`
|
1. Scan a directory: `sist2 scan ~/Documents --output ./documents.sist2`
|
||||||
2. Prepare search index:
|
2. Prepare search index:
|
||||||
* **Elasticsearch**: `sist2 index --es-url http://localhost:9200 ./documents.sist2`
|
* **Elasticsearch**: `sist2 index --es-url http://localhost:9200 ./documents.sist2`
|
||||||
* **SQLite**: `sist2 sqlite-index --search-index ./search.sist2 ./documents.sist2`
|
* **SQLite**: `sist2 index --search-index ./search.sist2 ./documents.sist2`
|
||||||
3. Start web interface:
|
3. Start web interface: `sist2 web ./documents.sist2`
|
||||||
* **Elasticsearch**: `sist2 web ./documents.sist2`
|
|
||||||
* **SQLite**: `sist2 web --search-index ./search.sist2 ./documents.sist2`
|
|
||||||
|
|
||||||
## Format support
|
## Format support
|
||||||
|
|
||||||
| File type | Library | Content | Thumbnail | Metadata |
|
| File type | Library | Content | Thumbnail | Metadata |
|
||||||
|:--------------------------------------------------------------------------|:-----------------------------------------------------------------------------|:---------|:------------|:---------------------------------------------------------------------------------------------------------------------------------------|
|
|:--------------------------------------------------------------------------|:-----------------------------------------------------------------------------|:---------|:------------|:---------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| pdf,xps,fb2,epub | MuPDF | text+ocr | yes | author, title |
|
| pdf,xps,fb2,epub | MuPDF | text+ocr | yes | author, title |
|
||||||
| cbz,cbr | [libscan](https://github.com/sist2app/sist2/tree/master/third-party/libscan) | - | yes | - |
|
| cbz,cbr | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | - | yes | - |
|
||||||
| `audio/*` | ffmpeg | - | yes | ID3 tags |
|
| `audio/*` | ffmpeg | - | yes | ID3 tags |
|
||||||
| `video/*` | ffmpeg | - | yes | title, comment, artist |
|
| `video/*` | ffmpeg | - | yes | title, comment, artist |
|
||||||
| `image/*` | ffmpeg | ocr | yes | [Common EXIF tags](https://github.com/sist2app/sist2/blob/efdde2734eca9b14a54f84568863b7ffd59bdba3/src/parsing/media.c#L190), GPS tags |
|
| `image/*` | ffmpeg | ocr | yes | [Common EXIF tags](https://github.com/simon987/sist2/blob/efdde2734eca9b14a54f84568863b7ffd59bdba3/src/parsing/media.c#L190), GPS tags |
|
||||||
| raw, rw2, dng, cr2, crw, dcr, k25, kdc, mrw, pef, xf3, arw, sr2, srf, erf | LibRaw | no | yes | Common EXIF tags, GPS tags |
|
| raw, rw2, dng, cr2, crw, dcr, k25, kdc, mrw, pef, xf3, arw, sr2, srf, erf | LibRaw | no | yes | Common EXIF tags, GPS tags |
|
||||||
| ttf,ttc,cff,woff,fnt,otf | Freetype2 | - | yes, `bmp` | Name & style |
|
| ttf,ttc,cff,woff,fnt,otf | Freetype2 | - | yes, `bmp` | Name & style |
|
||||||
| `text/plain` | [libscan](https://github.com/sist2app/sist2/tree/master/third-party/libscan) | yes | no | - |
|
| `text/plain` | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | no | - |
|
||||||
| html, xml | [libscan](https://github.com/sist2app/sist2/tree/master/third-party/libscan) | yes | no | - |
|
| html, xml | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | no | - |
|
||||||
| tar, zip, rar, 7z, ar ... | Libarchive | yes\* | - | no |
|
| tar, zip, rar, 7z, ar ... | Libarchive | yes\* | - | no |
|
||||||
| docx, xlsx, pptx | [libscan](https://github.com/sist2app/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 | yes | 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/sist2app/sist2/tree/master/third-party/libscan) | yes | - | - |
|
| json, jsonl, ndjson | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | - | - |
|
||||||
|
|
||||||
\* *See [Archive files](#archive-files)*
|
\* *See [Archive files](#archive-files)*
|
||||||
|
|
||||||
@ -135,7 +125,7 @@ You can enable OCR support for ebook (pdf,xps,fb2,epub) or image file types with
|
|||||||
Download the language data files with your package manager (`apt install tesseract-ocr-eng`) or
|
Download the language data files with your package manager (`apt install tesseract-ocr-eng`) or
|
||||||
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 `sist2app/sist2` image comes with common languages
|
The `simon987/sist2` image comes with common languages
|
||||||
(hin, jpn, eng, fra, rus, spa, chi_sim, deu, pol) 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
|
||||||
@ -156,17 +146,17 @@ sist2 v3.0.7+ supports SQLite search backend. The SQLite search backend has
|
|||||||
fewer features and generally comparable query performance for medium-size
|
fewer features and generally comparable query performance for medium-size
|
||||||
indices, but it uses much less memory and is easier to set up.
|
indices, but it uses much less memory and is easier to set up.
|
||||||
|
|
||||||
| | SQLite | Elasticsearch |
|
| | SQLite | Elasticsearch |
|
||||||
|----------------------------------------------|:---------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------:|
|
|----------------------------------------------|:----------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------:|
|
||||||
| Requires separate search engine installation | | ✓ |
|
| Requires separate search engine installation | | ✓ |
|
||||||
| Memory footprint | ~20MB | >500MB |
|
| 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) |
|
| 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 | | ✓ |
|
| Fuzzy search | | ✓ |
|
||||||
| Media Types tree real-time updating | | ✓ |
|
| Media Types tree real-time updating | | ✓ |
|
||||||
| Manual tagging | ✓ | ✓ |
|
| Search in file `path` | | ✓ |
|
||||||
| User scripts | ✓ | ✓ |
|
| Manual tagging | ✓ | ✓ |
|
||||||
| Media Type breakdown for search results | | ✓ |
|
| User scripts | | ✓ |
|
||||||
| Embeddings search | ✓ *O(n)* | ✓ *O(logn)* |
|
| Media Type breakdown for search results | | ✓ |
|
||||||
|
|
||||||
### NER
|
### NER
|
||||||
|
|
||||||
@ -175,13 +165,13 @@ sist2 v3.0.4+ supports named-entity recognition (NER). Simply add a supported re
|
|||||||
to enable it.
|
to enable it.
|
||||||
|
|
||||||
The text processing is done in your browser, no data is sent to any third-party services.
|
The text processing is done in your browser, no data is sent to any third-party services.
|
||||||
See [sist2app/sist2-ner-models](https://github.com/sist2app/sist2-ner-models) for more details.
|
See [simon987/sist2-ner-models](https://github.com/simon987/sist2-ner-models) for more details.
|
||||||
|
|
||||||
#### List of available repositories:
|
#### List of available repositories:
|
||||||
|
|
||||||
| URL | Maintainer | Purpose |
|
| URL | Maintainer | Purpose |
|
||||||
|---------------------------------------------------------------------------------------------------------|-----------------------------------------|---------|
|
|---------------------------------------------------------------------------------------------------------|-----------------------------------------|---------|
|
||||||
| [sist2app/sist2-ner-models](https://raw.githubusercontent.com/sist2app/sist2-ner-models/main/repo.json) | [sist2app](https://github.com/sist2app) | General |
|
| [simon987/sist2-ner-models](https://raw.githubusercontent.com/simon987/sist2-ner-models/main/repo.json) | [simon987](https://github.com/simon987) | General |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Screenshot</summary>
|
<summary>Screenshot</summary>
|
||||||
@ -197,7 +187,7 @@ You can compile **sist2** by yourself if you don't want to use the pre-compiled
|
|||||||
### Using docker
|
### Using docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone --recursive https://github.com/sist2app/sist2/
|
git clone --recursive https://github.com/simon987/sist2/
|
||||||
cd sist2
|
cd sist2
|
||||||
docker build . -t my-sist2-image
|
docker build . -t my-sist2-image
|
||||||
# Copy sist2 executable from docker image
|
# Copy sist2 executable from docker image
|
||||||
@ -212,16 +202,16 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install vcpkg using my fork: https://github.com/sist2app/vcpkg
|
2. Install vcpkg using my fork: https://github.com/simon987/vcpkg
|
||||||
3. Install vcpkg dependencies
|
3. Install vcpkg dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
vcpkg install openblas curl[core,openssl] sqlite3[core,fts5,json1] 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]
|
vcpkg install curl[core,openssl] sqlite3[core,fts5] 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,webp,opus,mp3lame,vpx,ffprobe,zlib]
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Build
|
4. Build
|
||||||
```bash
|
```bash
|
||||||
git clone --recursive https://github.com/sist2app/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)
|
||||||
(cd sist2-admin/frontend; npm install; npm run build)
|
(cd sist2-admin/frontend; npm install; npm run build)
|
||||||
cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE=<VCPKG_ROOT>/scripts/buildsystems/vcpkg.cmake .
|
cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE=<VCPKG_ROOT>/scripts/buildsystems/vcpkg.cmake .
|
||||||
|
@ -4,21 +4,16 @@ services:
|
|||||||
elasticsearch:
|
elasticsearch:
|
||||||
image: elasticsearch:7.17.9
|
image: elasticsearch:7.17.9
|
||||||
container_name: sist2-es
|
container_name: sist2-es
|
||||||
volumes:
|
|
||||||
# This directory must have 1000:1000 permissions (or update PUID & PGID below)
|
|
||||||
- /data/sist2-es-data/:/usr/share/elasticsearch/data
|
|
||||||
environment:
|
environment:
|
||||||
- "discovery.type=single-node"
|
- "discovery.type=single-node"
|
||||||
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||||
- "PUID=1000"
|
|
||||||
- "PGID=1000"
|
|
||||||
sist2-admin:
|
sist2-admin:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
container_name: sist2-admin
|
container_name: sist2-admin
|
||||||
volumes:
|
volumes:
|
||||||
- /data/sist2-admin-data/:/sist2-admin/
|
- /mnt/array/sist2-admin-data/:/sist2-admin/
|
||||||
- /<path to index>/:/host
|
- /:/host
|
||||||
ports:
|
ports:
|
||||||
- 4090:4090
|
- 4090:4090
|
||||||
# NOTE: Don't export this port publicly!
|
# NOTE: Don't export this port publicly!
|
||||||
|
@ -5,6 +5,7 @@ Usage: sist2 scan [OPTION]... PATH
|
|||||||
or: sist2 index [OPTION]... INDEX
|
or: sist2 index [OPTION]... INDEX
|
||||||
or: sist2 sqlite-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.
|
||||||
|
|
||||||
@ -16,9 +17,9 @@ Lightning-fast file system indexer and search tool.
|
|||||||
|
|
||||||
Scan options
|
Scan options
|
||||||
-t, --threads=<int> Number of threads. DEFAULT: 1
|
-t, --threads=<int> Number of threads. DEFAULT: 1
|
||||||
-q, --thumbnail_count-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 0 to 100, 100 being the best. DEFAULT: 50
|
||||||
--thumbnail_count-size=<int> Thumbnail size, in pixels. DEFAULT: 552
|
--thumbnail-size=<int> Thumbnail size, in pixels. DEFAULT: 552
|
||||||
--thumbnail_count-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
|
-o, --output=<str> Output index file path. DEFAULT: index.sist2
|
||||||
--incremental If the output file path exists, only scan new or modified files.
|
--incremental If the output file path exists, only scan new or modified files.
|
||||||
@ -73,14 +74,21 @@ 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-insecure-ssl Do not verify SSL connections to Elasticsearch.
|
||||||
|
--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
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Thumbnail database size estimation
|
#### Thumbnail database size estimation
|
||||||
|
|
||||||
See chart below for rough estimate of thumbnail_count size vs. thumbnail_count size & quality arguments:
|
See chart below for rough estimate of thumbnail size vs. thumbnail size & quality arguments:
|
||||||
|
|
||||||
For example, `--thumbnail_count-size=500`, `--thumbnail_count-quality=50` for a directory with 8 million images will create a thumbnail_count 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 * 11.8kB = 94.4GB`.
|
that is about `8000000 * 11.8kB = 94.4GB`.
|
||||||
|
|
||||||

|

|
||||||
@ -92,7 +100,7 @@ Simple scan
|
|||||||
sist2 scan ~/Documents
|
sist2 scan ~/Documents
|
||||||
|
|
||||||
sist2 scan \
|
sist2 scan \
|
||||||
--threads 4 --content-size 16000000 --thumbnail_count-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.sist2
|
~/Documents -o ./documents.sist2
|
||||||
```
|
```
|
||||||
@ -172,37 +180,12 @@ Using a version >=7.14.0 is recommended to enable the following features:
|
|||||||
|
|
||||||
- Bug fix for large documents (See #198)
|
- Bug fix for large documents (See #198)
|
||||||
|
|
||||||
Using a version >=8.0.0 is recommended to enable the following features:
|
|
||||||
|
|
||||||
- Approximate KNN search for Embeddings search (faster queries).
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
# Embeddings search
|
## exec-script
|
||||||
|
|
||||||
Since v3.2.0, User scripts can be used to generate _embeddings_ (vector of float32 numbers) which are stored in the .sist2 index file
|
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.
|
||||||
(see [scripting](scripting.md)). Embeddings can be used for:
|
|
||||||
|
|
||||||
* Nearest-neighbor queries (e.g. "return the documents most similar to this one")
|
|
||||||
* Semantic searches (e.g. "return the documents that are most closely related to the given topic")
|
|
||||||
|
|
||||||
In theory, embeddings can be created for any type of documents (image, text, audio etc.).
|
|
||||||
|
|
||||||
For example, the [clip](https://github.com/sist2app/sist2-script-clip) User Script, generates 512-d embeddings of images
|
|
||||||
(videos are also supported using the thumbnails generated by sist2). When the user enters a query in the "Embeddings Search"
|
|
||||||
textbox, the query's embedding is generated in their browser, leveraging the ONNX web runtime.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Screenshots</summary>
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
1. Embeddings search bar. You can select the model using the dropdown on the left.
|
|
||||||
2. This icon appears for indices with embeddings search enabled.
|
|
||||||
3. Documents with this icon have embeddings. Click on the icon to perform KNN search.
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
# Tagging
|
# Tagging
|
||||||
@ -230,3 +213,42 @@ See [Automatic tagging](#automatic-tagging) for information about tag
|
|||||||
### Automatic tagging
|
### Automatic tagging
|
||||||
|
|
||||||
See [scripting](scripting.md) documentation.
|
See [scripting](scripting.md) documentation.
|
||||||
|
|
||||||
|
# Sidecar files
|
||||||
|
|
||||||
|
When scanning, sist2 will read metadata from `.s2meta` JSON files and overwrite the
|
||||||
|
original document's indexed metadata (does not modify the actual file). Sidecar metadata files will also work inside archives.
|
||||||
|
Sidecar files themselves are not saved in the index.
|
||||||
|
|
||||||
|
This feature is useful to leverage third-party applications such as speech-to-text or
|
||||||
|
OCR to add additional metadata to a file.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```
|
||||||
|
~/Documents/
|
||||||
|
├── Video.mp4
|
||||||
|
└── Video.mp4.s2meta
|
||||||
|
```
|
||||||
|
|
||||||
|
The sidecar file must have exactly the same file path and the `.s2meta` suffix.
|
||||||
|
|
||||||
|
`Video.mp4.s2meta`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "This sidecar file will overwrite some metadata fields of Video.mp4",
|
||||||
|
"author": "Some author",
|
||||||
|
"duration": 12345,
|
||||||
|
"bitrate": 67890,
|
||||||
|
"some_arbitrary_field": [1,2,3]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
sist2 scan ~/Documents -o ./docs.sist2
|
||||||
|
sist2 index ./docs.sist2
|
||||||
|
```
|
||||||
|
|
||||||
|
*NOTE*: It is technically possible to overwrite the `tag` value using sidecar files, however,
|
||||||
|
it is not currently possible to restore both manual tags and sidecar tags without user scripts
|
||||||
|
while reindexing.
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 90 KiB |
Binary file not shown.
Before Width: | Height: | Size: 996 KiB |
@ -1,47 +1,18 @@
|
|||||||
## User scripts
|
## User scripts
|
||||||
|
|
||||||
User scripts are used to augment your sist2 index with additional metadata, neural network embeddings, tags etc.
|
*This document is under construction, more in-depth guide coming soon*
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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",
|
||||||
@ -63,8 +34,7 @@ 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();
|
||||||
|
|
||||||
@ -77,23 +47,21 @@ You can use `.` to create a hierarchical tag tree:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
@ -104,7 +72,6 @@ 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();
|
||||||
|
|
||||||
@ -117,9 +84,8 @@ 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();
|
||||||
|
|
||||||
@ -136,18 +102,16 @@ 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(' ');
|
||||||
@ -160,7 +124,6 @@ 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");
|
||||||
@ -177,6 +140,3 @@ if (ctx._source?.exif_datetime != null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 78 KiB |
@ -202,46 +202,6 @@
|
|||||||
},
|
},
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
VCPKG_ROOT="/vcpkg"
|
VCPKG_ROOT="/vcpkg"
|
||||||
|
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
(
|
(
|
||||||
cd sist2-vue/
|
cd sist2-vue/
|
||||||
npm install
|
npm install
|
||||||
|
@ -1,16 +1,8 @@
|
|||||||
|
|
||||||
MAGIC_PATHS = [
|
try:
|
||||||
"/vcpkg/installed/x64-linux/share/libmagic/misc/magic.mgc",
|
with open("/usr/lib/file/magic.mgc", "rb") as f:
|
||||||
"/work/vcpkg/installed/x64-linux/share/libmagic/misc/magic.mgc",
|
data = f.read()
|
||||||
"/usr/lib/file/magic.mgc"
|
except:
|
||||||
]
|
data = bytes([])
|
||||||
|
|
||||||
for path in MAGIC_PATHS:
|
|
||||||
try:
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
data = f.read()
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
print("char magic_database_buffer[%d] = {%s};" % (len(data), ",".join(str(int(b)) for b in data)))
|
print("char magic_database_buffer[%d] = {%s};" % (len(data), ",".join(str(int(b)) for b in data)))
|
||||||
|
@ -449,4 +449,5 @@ image/x-sigma-x3f, xf3
|
|||||||
image/x-sony-arw, arw
|
image/x-sony-arw, arw
|
||||||
image/x-sony-sr2, sr2
|
image/x-sony-sr2, sr2
|
||||||
image/x-sony-srf, srf
|
image/x-sony-srf, srf
|
||||||
image/x-epson-erf, erf
|
image/x-epson-erf, erf
|
||||||
|
sist2/sidecar, s2meta
|
|
@ -3,7 +3,6 @@ import zlib
|
|||||||
mimes = {}
|
mimes = {}
|
||||||
noparse = set()
|
noparse = set()
|
||||||
ext_in_hash = set()
|
ext_in_hash = set()
|
||||||
mime_ids = {}
|
|
||||||
|
|
||||||
major_mime = {
|
major_mime = {
|
||||||
"sist2": 0,
|
"sist2": 0,
|
||||||
@ -103,9 +102,6 @@ cnt = 1
|
|||||||
|
|
||||||
|
|
||||||
def mime_id(mime):
|
def mime_id(mime):
|
||||||
if mime in mime_ids:
|
|
||||||
return mime_ids[mime]
|
|
||||||
|
|
||||||
global cnt
|
global cnt
|
||||||
major = mime.split("/")[0]
|
major = mime.split("/")[0]
|
||||||
mime_id = str((major_mime[major] << 16) + cnt)
|
mime_id = str((major_mime[major] << 16) + cnt)
|
||||||
@ -131,7 +127,9 @@ def mime_id(mime):
|
|||||||
elif mime == "application/x-empty":
|
elif mime == "application/x-empty":
|
||||||
cnt -= 1
|
cnt -= 1
|
||||||
return "1"
|
return "1"
|
||||||
mime_ids[mime] = mime_id
|
elif mime == "sist2/sidecar":
|
||||||
|
cnt -= 1
|
||||||
|
return "2"
|
||||||
return mime_id
|
return mime_id
|
||||||
|
|
||||||
|
|
||||||
@ -199,12 +197,4 @@ with open("scripts/mime.csv") as f:
|
|||||||
print(f"case {crc(mime)}: return {clean(mime)};")
|
print(f"case {crc(mime)}: return {clean(mime)};")
|
||||||
|
|
||||||
print("default: return 0;}}")
|
print("default: return 0;}}")
|
||||||
|
|
||||||
# mime list
|
|
||||||
|
|
||||||
mime_list = ",".join(mime_id(x) for x in mimes.keys()) + ",0"
|
|
||||||
|
|
||||||
print(f"unsigned int mime_ids[] = {{{mime_list}}};")
|
|
||||||
print("unsigned int* get_mime_ids() { return mime_ids; }")
|
|
||||||
|
|
||||||
print("#endif")
|
print("#endif")
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
docker run --rm -it --name "sist2-dev-es3"\
|
docker run --rm -it --name "sist2-dev-es"\
|
||||||
-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
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
docker run --rm -it --name "sist2-dev-es3"\
|
docker run --rm -it --name "sist2-dev-es"\
|
||||||
-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.7.0
|
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:8.7.0
|
||||||
|
671
sist2-admin/frontend/package-lock.json
generated
671
sist2-admin/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@
|
|||||||
"watch": "vue-cli-service build --watch"
|
"watch": "vue-cli-service build --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^0.27.2",
|
||||||
"bootstrap-vue": "^2.21.2",
|
"bootstrap-vue": "^2.21.2",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"moment": "^2.29.3",
|
"moment": "^2.29.3",
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
<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/sist2app/sist2/issues/new/choose" target="_blank">issue tracker on
|
to the <a href="https://github.com/simon987/sist2/issues/new/choose" target="_blank">issue tracker on
|
||||||
Github</a>. Thank you!
|
Github</a>. Thank you!
|
||||||
</b-alert>
|
</b-alert>
|
||||||
<router-view v-if="$store.state.sist2AdminInfo"/>
|
<router-view/>
|
||||||
</b-container>
|
</b-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -71,12 +71,10 @@ html, body {
|
|||||||
|
|
||||||
.info-icon {
|
.info-icon {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
min-width: 1rem;
|
|
||||||
margin-right: 0.2rem;
|
margin-right: 0.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
min-height: 1rem;
|
|
||||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==);
|
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==);
|
||||||
filter: brightness(45%);
|
filter: brightness(45%);
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -89,12 +89,9 @@ class Sist2AdminApi {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {bool} full
|
|
||||||
*/
|
*/
|
||||||
runJob(name, full) {
|
runJob(name) {
|
||||||
return axios.get(`${this.baseUrl}/api/job/${name}/run`, {
|
return axios.get(`${this.baseUrl}/api/job/${name}/run`);
|
||||||
params: {full}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -142,38 +139,6 @@ class Sist2AdminApi {
|
|||||||
deleteTaskLogs(taskId) {
|
deleteTaskLogs(taskId) {
|
||||||
return axios.post(`${this.baseUrl}/api/task/${taskId}/delete_logs`);
|
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Sist2AdminApi()
|
export default new Sist2AdminApi()
|
@ -1,54 +1,42 @@
|
|||||||
<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'"
|
<b-form-checkbox :disabled="job.status !== 'indexed'" :value="job.name">[{{ job.name }}]</b-form-checkbox>
|
||||||
:value="job.name">
|
<br/>
|
||||||
<template #default><span
|
</div>
|
||||||
:title="job.status !== 'indexed' ? $t('jobOptions.notIndexed') : ''"
|
</b-form-checkbox-group>
|
||||||
>[{{ job.name }}]</span></template>
|
<div v-else>
|
||||||
</b-form-checkbox>
|
<span class="text-muted">{{ $t('jobOptions.noJobAvailable') }}</span>
|
||||||
<br/>
|
<router-link to="/">{{$t("create")}}</router-link>
|
||||||
</div>
|
</div>
|
||||||
</b-form-checkbox-group>
|
</b-form-group>
|
||||||
<div v-else>
|
</div>
|
||||||
<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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
computed: {
|
data() {
|
||||||
jobs() {
|
return {
|
||||||
return this._jobs
|
loading: true,
|
||||||
.filter(job => job.index_options.search_backend === this.frontend.web_options.search_backend)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loading: true,
|
|
||||||
_jobs: null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -1,34 +0,0 @@
|
|||||||
<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>
|
|
@ -95,7 +95,6 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
onOcrLangChange() {
|
onOcrLangChange() {
|
||||||
this.options.ocr_lang = this.selectedOcrLangs.join("+");
|
this.options.ocr_lang = this.selectedOcrLangs.join("+");
|
||||||
this.update();
|
|
||||||
},
|
},
|
||||||
update() {
|
update() {
|
||||||
this.disableOcrLang = this.options.ocr_images === false && this.options.ocr_ebooks === false;
|
this.disableOcrLang = this.options.ocr_images === false && this.options.ocr_ebooks === false;
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
<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>
|
|
@ -1,88 +0,0 @@
|
|||||||
<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>
|
|
@ -1,70 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h4>{{ $t("webOptions.title") }}</h4>
|
|
||||||
<b-card>
|
|
||||||
<label>{{ $t("webOptions.lang") }}</label>
|
|
||||||
<b-form-select v-model="options.lang" :options="['en', 'fr', 'zh-CN', 'pl', 'de']"
|
|
||||||
@change="update()"></b-form-select>
|
|
||||||
|
|
||||||
<label>{{ $t("webOptions.bind") }}</label>
|
<label>{{ $t("webOptions.lang") }}</label>
|
||||||
<b-form-input v-model="options.bind" @change="update()"></b-form-input>
|
<b-form-select v-model="options.lang" :options="['en', 'fr', 'zh-CN']" @change="update()"></b-form-select>
|
||||||
|
|
||||||
<label>{{ $t("webOptions.tagline") }}</label>
|
<label>{{ $t("webOptions.bind") }}</label>
|
||||||
<b-form-textarea v-model="options.tagline" @change="update()"></b-form-textarea>
|
<b-form-input v-model="options.bind" @change="update()"></b-form-input>
|
||||||
|
|
||||||
<label>{{ $t("webOptions.auth") }}</label>
|
<label>{{ $t("webOptions.tagline") }}</label>
|
||||||
<b-form-input v-model="options.auth" @change="update()"></b-form-input>
|
<b-form-textarea v-model="options.tagline" @change="update()"></b-form-textarea>
|
||||||
|
|
||||||
<label>{{ $t("webOptions.tagAuth") }}</label>
|
<label>{{ $t("webOptions.auth") }}</label>
|
||||||
<b-form-input v-model="options.tag_auth" @change="update()" :disabled="Boolean(options.auth)"></b-form-input>
|
<b-form-input v-model="options.auth" @change="update()"></b-form-input>
|
||||||
|
|
||||||
<b-form-checkbox v-model="options.verbose" @change="update()">
|
<label>{{ $t("webOptions.tagAuth") }}</label>
|
||||||
{{$t("webOptions.verbose")}}
|
<b-form-input v-model="options.tag_auth" @change="update()"></b-form-input>
|
||||||
</b-form-checkbox>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<h4>Auth0 options</h4>
|
<h5>Auth0 options</h5>
|
||||||
<b-card>
|
<label>{{ $t("webOptions.auth0Audience") }}</label>
|
||||||
<label>{{ $t("webOptions.auth0Audience") }}</label>
|
<b-form-input v-model="options.auth0_audience" @change="update()"></b-form-input>
|
||||||
<b-form-input v-model="options.auth0_audience" @change="update()"></b-form-input>
|
|
||||||
|
|
||||||
<label>{{ $t("webOptions.auth0Domain") }}</label>
|
<label>{{ $t("webOptions.auth0Domain") }}</label>
|
||||||
<b-form-input v-model="options.auth0_domain" @change="update()"></b-form-input>
|
<b-form-input v-model="options.auth0_domain" @change="update()"></b-form-input>
|
||||||
|
|
||||||
<label>{{ $t("webOptions.auth0ClientId") }}</label>
|
<label>{{ $t("webOptions.auth0ClientId") }}</label>
|
||||||
<b-form-input v-model="options.auth0_client_id" @change="update()"></b-form-input>
|
<b-form-input v-model="options.auth0_client_id" @change="update()"></b-form-input>
|
||||||
|
|
||||||
<label>{{ $t("webOptions.auth0PublicKey") }}</label>
|
<label>{{ $t("webOptions.auth0PublicKey") }}</label>
|
||||||
<b-textarea rows="10" v-model="options.auth0_public_key" @change="update()"></b-textarea>
|
<b-textarea rows="10" v-model="options.auth0_public_key" @change="update()"></b-textarea>
|
||||||
</b-card>
|
</div>
|
||||||
</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() {
|
|
||||||
|
|
||||||
console.log(this.options)
|
|
||||||
if (this.options.auth && this.options.tag_auth) {
|
|
||||||
// If both are set, remove tagAuth
|
|
||||||
this.options.tag_auth = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit("change", this.options);
|
|
||||||
},
|
},
|
||||||
}
|
methods: {
|
||||||
|
update() {
|
||||||
|
if (!this.options.es_url.startsWith("https")) {
|
||||||
|
this.options.es_insecure_ssl = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit("change", this.options);
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ export default {
|
|||||||
view: "View",
|
view: "View",
|
||||||
delete: "Delete",
|
delete: "Delete",
|
||||||
runNow: "Index now",
|
runNow: "Index now",
|
||||||
runNowFull: "Full re-index",
|
|
||||||
create: "Create",
|
create: "Create",
|
||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
test: "Test",
|
test: "Test",
|
||||||
@ -53,23 +52,7 @@ export default {
|
|||||||
searchBackendTitle: "search backend configuration",
|
searchBackendTitle: "search backend configuration",
|
||||||
newBackendName: "New search backend name",
|
newBackendName: "New search backend name",
|
||||||
|
|
||||||
frontendTab: "Frontend",
|
selectJobs: "Select jobs",
|
||||||
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",
|
|
||||||
|
|
||||||
couldNotStartFrontend: "Could not start frontend",
|
|
||||||
couldNotStartFrontendBody: "Unable to start the frontend, check server logs for more details.",
|
|
||||||
|
|
||||||
selectJobs: "Available jobs",
|
|
||||||
selectJob: "Select a job",
|
|
||||||
webOptions: {
|
webOptions: {
|
||||||
title: "Web options",
|
title: "Web options",
|
||||||
lang: "UI Language",
|
lang: "UI Language",
|
||||||
@ -81,7 +64,6 @@ export default {
|
|||||||
auth0Domain: "Auth0 domain",
|
auth0Domain: "Auth0 domain",
|
||||||
auth0ClientId: "Auth0 client ID",
|
auth0ClientId: "Auth0 client ID",
|
||||||
auth0PublicKey: "Auth0 public key",
|
auth0PublicKey: "Auth0 public key",
|
||||||
verbose: "Verbose logs"
|
|
||||||
},
|
},
|
||||||
backendOptions: {
|
backendOptions: {
|
||||||
title: "Search backend options",
|
title: "Search backend options",
|
||||||
@ -127,13 +109,12 @@ export default {
|
|||||||
keepNLogs: "Keep last N log files. Set to -1 to keep all logs.",
|
keepNLogs: "Keep last N log files. Set to -1 to keep all logs.",
|
||||||
deleteNow: "Delete now",
|
deleteNow: "Delete now",
|
||||||
scheduleEnabled: "Enable scheduled re-scan",
|
scheduleEnabled: "Enable scheduled re-scan",
|
||||||
noJobAvailable: "No jobs available for this search backend.",
|
noJobAvailable: "No jobs available.",
|
||||||
notIndexed: "Has not been indexed yet",
|
|
||||||
noBackendError: "You must select a search backend to run this job",
|
noBackendError: "You must select a search backend to run this job",
|
||||||
desktopNotifications: "Desktop notifications"
|
desktopNotifications: "Desktop notifications"
|
||||||
},
|
},
|
||||||
frontendOptions: {
|
frontendOptions: {
|
||||||
title: "Advanced options",
|
title: "Frontend 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: {
|
||||||
|
@ -6,18 +6,12 @@ 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 SearchBackend from "@/views/SearchBackend.vue";
|
||||||
import UserScript from "@/views/UserScript.vue";
|
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: "/task",
|
path: "/",
|
||||||
name: "Tasks",
|
|
||||||
component: Tasks
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/:tab?",
|
|
||||||
name: "Home",
|
name: "Home",
|
||||||
component: Home
|
component: Home
|
||||||
},
|
},
|
||||||
@ -26,6 +20,11 @@ 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",
|
||||||
@ -36,11 +35,6 @@ const routes = [
|
|||||||
name: "SearchBackend",
|
name: "SearchBackend",
|
||||||
component: SearchBackend
|
component: SearchBackend
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/userScript/:name",
|
|
||||||
name: "UserScript",
|
|
||||||
component: UserScript
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/log/:taskId",
|
path: "/log/:taskId",
|
||||||
name: "Tail",
|
name: "Tail",
|
||||||
|
@ -1,63 +1,70 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<!-- Action buttons-->
|
<div class="mb-3" v-if="!loading">
|
||||||
<div class="mb-3" v-if="!loading">
|
<b-button class="mr-1" :disabled="frontend.running || !valid" variant="success" @click="start()">{{
|
||||||
<b-button class="mr-1" :disabled="frontend.running || !valid" variant="success" @click="start()">{{
|
$t("start")
|
||||||
$t("start")
|
}}
|
||||||
}}
|
</b-button>
|
||||||
</b-button>
|
<b-button class="mr-1" :disabled="!frontend.running" variant="danger" @click="stop()">{{
|
||||||
<b-button class="mr-1" :disabled="!frontend.running" variant="danger" @click="stop()">{{
|
$t("stop")
|
||||||
$t("stop")
|
}}
|
||||||
}}
|
</b-button>
|
||||||
</b-button>
|
<b-button class="mr-1" :disabled="!frontend.running" variant="primary" :href="frontendUrl" target="_blank">
|
||||||
<b-button class="mr-1" :disabled="!frontend.running" variant="primary" :href="frontendUrl" target="_blank">
|
{{ $t("go") }}
|
||||||
{{ $t("go") }}
|
</b-button>
|
||||||
</b-button>
|
<b-button variant="danger" @click="deleteFrontend()">{{ $t("delete") }}</b-button>
|
||||||
<b-button variant="danger" @click="deleteFrontend()">{{ $t("delete") }}</b-button>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<b-progress v-if="loading" striped animated value="100"></b-progress>
|
|
||||||
<b-card-body v-else>
|
|
||||||
|
|
||||||
<h4>{{ $t("backendOptions.title") }}</h4>
|
<b-progress v-if="loading" striped animated value="100"></b-progress>
|
||||||
<b-card>
|
<b-card-body v-else>
|
||||||
<b-alert v-if="!valid" variant="warning" show>{{ $t("frontendOptions.noJobSelectedWarning") }}</b-alert>
|
|
||||||
|
|
||||||
<SearchBackendSelect :value="frontend.web_options.search_backend"
|
<h4>{{ $t("frontendOptions.title") }}</h4>
|
||||||
@change="onBackendSelect($event)"></SearchBackendSelect>
|
<b-card>
|
||||||
|
<b-form-checkbox v-model="frontend.auto_start" @change="update()">
|
||||||
|
{{ $t("autoStart") }}
|
||||||
|
</b-form-checkbox>
|
||||||
|
|
||||||
<br>
|
<label>{{ $t("extraQueryArgs") }}</label>
|
||||||
<JobCheckboxGroup :frontend="frontend" @input="update()"></JobCheckboxGroup>
|
<b-form-input v-model="frontend.extra_query_args" @change="update()"></b-form-input>
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<br/>
|
<label>{{ $t("customUrl") }}</label>
|
||||||
|
<b-form-input v-model="frontend.custom_url" @change="update()" placeholder="http://"></b-form-input>
|
||||||
|
|
||||||
<WebOptions :options="frontend.web_options" :frontend-name="$route.params.name"
|
<br/>
|
||||||
@change="update()"></WebOptions>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<h4>{{ $t("frontendOptions.title") }}</h4>
|
<b-alert v-if="!valid" variant="warning" show>{{ $t("frontendOptions.noJobSelectedWarning") }}</b-alert>
|
||||||
<b-card>
|
|
||||||
<b-form-checkbox v-model="frontend.auto_start" @change="update()">
|
|
||||||
{{ $t("autoStart") }}
|
|
||||||
</b-form-checkbox>
|
|
||||||
|
|
||||||
<label>{{ $t("extraQueryArgs") }}</label>
|
<JobCheckboxGroup :frontend="frontend" @input="update()"></JobCheckboxGroup>
|
||||||
<b-form-input v-model="frontend.extra_query_args" @change="update()"></b-form-input>
|
</b-card>
|
||||||
|
|
||||||
<label>{{ $t("customUrl") }}</label>
|
<br/>
|
||||||
<b-form-input v-model="frontend.custom_url" @change="update()" placeholder="http://"></b-form-input>
|
|
||||||
</b-card>
|
<h4>{{ $t("webOptions.title") }}</h4>
|
||||||
</b-card-body>
|
<b-card>
|
||||||
</b-card>
|
<WebOptions :options="frontend.web_options" :frontend-name="$route.params.name"
|
||||||
|
@change="update()"></WebOptions>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h4>{{ $t("backendOptions.title") }}</h4>
|
||||||
|
<b-card>
|
||||||
|
<SearchBackendSelect :value="frontend.web_options.search_backend"
|
||||||
|
@change="onBackendSelect($event)"></SearchBackendSelect>
|
||||||
|
</b-card>
|
||||||
|
</b-card-body>
|
||||||
|
|
||||||
|
|
||||||
|
</b-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -68,78 +75,70 @@ import WebOptions from "@/components/WebOptions";
|
|||||||
import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
|
import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Frontend',
|
name: 'Frontend',
|
||||||
components: {SearchBackendSelect, 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;
|
||||||
port() {
|
this.loading = false;
|
||||||
return this.frontend.web_options.bind.split(":")[1]
|
|
||||||
},
|
|
||||||
args() {
|
|
||||||
const args = this.frontend.extra_query_args;
|
|
||||||
if (args !== "") {
|
|
||||||
return "#" + (args.startsWith("?") ? (args) : ("?" + args));
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
Sist2AdminApi.getFrontend(this.name).then(resp => {
|
|
||||||
this.frontend = resp.data;
|
|
||||||
this.loading = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
start() {
|
|
||||||
Sist2AdminApi.startFrontend(this.name).then(() => {
|
|
||||||
this.frontend.running = true;
|
|
||||||
}).catch(() => {
|
|
||||||
this.$bvToast.toast(this.$t("couldNotStartFrontendBody"), {
|
|
||||||
title: this.$t("couldNotStartFrontend"),
|
|
||||||
variant: "danger",
|
|
||||||
toaster: "b-toaster-bottom-right"
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
},
|
},
|
||||||
stop() {
|
methods: {
|
||||||
this.frontend.running = false;
|
start() {
|
||||||
Sist2AdminApi.stopFrontend(this.name)
|
this.frontend.running = true;
|
||||||
},
|
Sist2AdminApi.startFrontend(this.name)
|
||||||
deleteFrontend() {
|
},
|
||||||
Sist2AdminApi.deleteFrontend(this.name).then(() => {
|
stop() {
|
||||||
this.$router.push("/");
|
this.frontend.running = false;
|
||||||
});
|
Sist2AdminApi.stopFrontend(this.name)
|
||||||
},
|
},
|
||||||
update() {
|
deleteFrontend() {
|
||||||
Sist2AdminApi.updateFrontend(this.name, this.frontend);
|
Sist2AdminApi.deleteFrontend(this.name).then(() => {
|
||||||
},
|
this.$router.push("/");
|
||||||
onBackendSelect(backend) {
|
});
|
||||||
this.frontend.web_options.search_backend = backend;
|
},
|
||||||
this.frontend.jobs = [];
|
update() {
|
||||||
this.update();
|
Sist2AdminApi.updateFrontend(this.name, this.frontend);
|
||||||
|
},
|
||||||
|
onBackendSelect(backend) {
|
||||||
|
this.frontend.web_options.search_backend = backend;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -1,122 +1,88 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<b-tabs content-class="mt-3" v-model="tab" @input="onTabChange($event)">
|
<b-card>
|
||||||
<b-tab :title="$t('backendTab')">
|
<b-card-title>{{ $t("jobs") }}</b-card-title>
|
||||||
|
<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>
|
||||||
|
|
||||||
<b-card>
|
<hr/>
|
||||||
<b-card-title>{{ $t("searchBackends") }}</b-card-title>
|
|
||||||
|
|
||||||
<b-row>
|
<b-progress v-if="jobsLoading" striped animated value="100"></b-progress>
|
||||||
<b-col>
|
<b-list-group v-else>
|
||||||
<b-input v-model="newBackendName" :placeholder="$t('newBackendName')"></b-input>
|
<JobListItem v-for="job in jobs" :key="job.name" :job="job"></JobListItem>
|
||||||
</b-col>
|
</b-list-group>
|
||||||
<b-col>
|
</b-card>
|
||||||
<b-button variant="primary" @click="createBackend()"
|
|
||||||
:disabled="!backendNameValid(newBackendName)">
|
|
||||||
{{ $t("create") }}
|
|
||||||
</b-button>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
|
|
||||||
<hr/>
|
<br/>
|
||||||
|
|
||||||
<b-progress v-if="backendsLoading" striped animated value="100"></b-progress>
|
<b-card>
|
||||||
<b-list-group v-else>
|
|
||||||
<SearchBackendListItem v-for="backend in backends"
|
|
||||||
:key="backend.name" :backend="backend"></SearchBackendListItem>
|
|
||||||
</b-list-group>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<br/>
|
<b-card-title>{{ $t("frontends") }}</b-card-title>
|
||||||
|
|
||||||
<b-card>
|
<b-row>
|
||||||
<b-card-title>{{ $t("jobs") }}</b-card-title>
|
<b-col>
|
||||||
<b-row>
|
<b-input v-model="newFrontendName" :placeholder="$t('newFrontendName')"></b-input>
|
||||||
<b-col>
|
</b-col>
|
||||||
<b-input id="new-job" v-model="newJobName" :placeholder="$t('newJobName')"></b-input>
|
<b-col>
|
||||||
<b-popover
|
<b-button variant="primary" @click="createFrontend()"
|
||||||
:show.sync="showHelp"
|
:disabled="!frontendNameValid(newFrontendName)">
|
||||||
target="new-job"
|
{{ $t("create") }}
|
||||||
placement="top"
|
</b-button>
|
||||||
triggers="manual"
|
</b-col>
|
||||||
variant="primary"
|
</b-row>
|
||||||
: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="jobsLoading" striped animated value="100"></b-progress>
|
<b-progress v-if="frontendsLoading" striped animated value="100"></b-progress>
|
||||||
<b-list-group v-else>
|
<b-list-group v-else>
|
||||||
<JobListItem v-for="job in jobs" :key="job.name" :job="job"></JobListItem>
|
<FrontendListItem v-for="frontend in frontends"
|
||||||
</b-list-group>
|
:key="frontend.name" :frontend="frontend"></FrontendListItem>
|
||||||
</b-card>
|
</b-list-group>
|
||||||
</b-tab>
|
|
||||||
<b-tab :title="$t('scripts')">
|
|
||||||
|
|
||||||
<b-progress v-if="scriptsLoading" striped animated value="100"></b-progress>
|
</b-card>
|
||||||
<b-card v-else>
|
|
||||||
<b-card-title>{{ $t("scripts") }}</b-card-title>
|
|
||||||
|
|
||||||
<label>Select template</label>
|
<br/>
|
||||||
<b-form-radio-group stacked :options="scriptTemplates" v-model="scriptTemplate"></b-form-radio-group>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<b-row>
|
<b-card>
|
||||||
<b-col>
|
<b-card-title>{{ $t("searchBackends") }}</b-card-title>
|
||||||
<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-row>
|
||||||
|
<b-col>
|
||||||
|
<b-input v-model="newBackendName" :placeholder="$t('newBackendName')"></b-input>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-button variant="primary" @click="createBackend()"
|
||||||
|
:disabled="!backendNameValid(newBackendName)">
|
||||||
|
{{ $t("create") }}
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
<b-list-group>
|
<hr/>
|
||||||
<UserScriptListItem v-for="script in scripts"
|
|
||||||
:key="script.name" :script="script"></UserScriptListItem>
|
|
||||||
</b-list-group>
|
|
||||||
|
|
||||||
</b-card>
|
<b-progress v-if="backendsLoading" striped animated value="100"></b-progress>
|
||||||
</b-tab>
|
<b-list-group v-else>
|
||||||
<b-tab :title="$t('frontendTab')">
|
<SearchBackendListItem v-for="backend in backends"
|
||||||
<b-card>
|
:key="backend.name" :backend="backend"></SearchBackendListItem>
|
||||||
|
</b-list-group>
|
||||||
|
|
||||||
<b-card-title>{{ $t("frontends") }}</b-card-title>
|
</b-card>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -126,11 +92,10 @@ 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 SearchBackendListItem from "@/components/SearchBackendListItem.vue";
|
||||||
import UserScriptListItem from "@/components/UserScriptListItem.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Jobs",
|
name: "Jobs",
|
||||||
components: {UserScriptListItem, SearchBackendListItem, JobListItem, FrontendListItem},
|
components: {SearchBackendListItem, JobListItem, FrontendListItem},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
jobsLoading: true,
|
jobsLoading: true,
|
||||||
@ -146,24 +111,11 @@ export default {
|
|||||||
backendsLoading: true,
|
backendsLoading: true,
|
||||||
newBackendName: "",
|
newBackendName: "",
|
||||||
|
|
||||||
scripts: [],
|
showHelp: false
|
||||||
scriptTemplates: [],
|
|
||||||
newScriptName: "",
|
|
||||||
scriptTemplate: null,
|
|
||||||
scriptsLoading: true,
|
|
||||||
|
|
||||||
showHelp: false,
|
|
||||||
tab: 0
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loading = true;
|
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();
|
this.reload();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -188,20 +140,11 @@ export default {
|
|||||||
|
|
||||||
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
|
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() {
|
reload() {
|
||||||
Sist2AdminApi.getJobs().then(resp => {
|
Sist2AdminApi.getJobs().then(resp => {
|
||||||
this.jobs = resp.data;
|
this.jobs = resp.data;
|
||||||
this.jobsLoading = false;
|
this.jobsLoading = false;
|
||||||
|
|
||||||
this.showHelp = this.jobs.length === 0;
|
this.showHelp = this.jobs.length === 0;
|
||||||
});
|
});
|
||||||
Sist2AdminApi.getFrontends().then(resp => {
|
Sist2AdminApi.getFrontends().then(resp => {
|
||||||
@ -212,11 +155,6 @@ export default {
|
|||||||
this.backends = resp.data;
|
this.backends = resp.data;
|
||||||
this.backendsLoading = false;
|
this.backendsLoading = false;
|
||||||
})
|
})
|
||||||
Sist2AdminApi.getUserScripts().then(resp => {
|
|
||||||
this.scripts = resp.data;
|
|
||||||
this.scriptTemplates = this.$store.state.sist2AdminInfo.user_script_templates;
|
|
||||||
this.scriptsLoading = false;
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
createJob() {
|
createJob() {
|
||||||
Sist2AdminApi.createJob(this.newJobName).then(this.reload);
|
Sist2AdminApi.createJob(this.newJobName).then(this.reload);
|
||||||
@ -226,14 +164,6 @@ export default {
|
|||||||
},
|
},
|
||||||
createBackend() {
|
createBackend() {
|
||||||
Sist2AdminApi.createBackend(this.newBackendName).then(this.reload);
|
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}})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,19 +6,7 @@
|
|||||||
</b-card-title>
|
</b-card-title>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
<b-button class="mr-1" variant="primary" @click="runJob()" :disabled="!valid">{{ $t("runNow") }}</b-button>
|
||||||
<b-dropdown
|
|
||||||
split
|
|
||||||
split-variant="primary"
|
|
||||||
variant="primary"
|
|
||||||
:text="$t('runNow')"
|
|
||||||
class="mr-1"
|
|
||||||
:disabled="!valid"
|
|
||||||
@click="runJob()"
|
|
||||||
>
|
|
||||||
<b-dropdown-item href="#" @click="runJob(true)">{{ $t("runNowFull") }}</b-dropdown-item>
|
|
||||||
</b-dropdown>
|
|
||||||
|
|
||||||
<b-button variant="danger" @click="deleteJob()">{{ $t("delete") }}</b-button>
|
<b-button variant="danger" @click="deleteJob()">{{ $t("delete") }}</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -36,26 +24,19 @@
|
|||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
<h4>{{ $t("scanOptions.title") }}</h4>
|
||||||
|
<b-card>
|
||||||
|
<ScanOptions :options="job.scan_options" @change="update()"></ScanOptions>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
<h4>{{ $t("backendOptions.title") }}</h4>
|
<h4>{{ $t("backendOptions.title") }}</h4>
|
||||||
<b-card>
|
<b-card>
|
||||||
<b-alert v-if="!valid" variant="warning" show>{{ $t("jobOptions.noBackendError") }}</b-alert>
|
<b-alert v-if="!valid" variant="warning" show>{{ $t("jobOptions.noBackendError") }}</b-alert>
|
||||||
<SearchBackendSelect :value="job.index_options.search_backend"
|
<SearchBackendSelect :value="job.index_options.search_backend"
|
||||||
@change="onBackendSelect($event)"></SearchBackendSelect>
|
@change="onBackendSelect($event)"></SearchBackendSelect>
|
||||||
</b-card>
|
</b-card>
|
||||||
<br/>
|
|
||||||
|
|
||||||
<h4>{{ $t("scriptOptions") }}</h4>
|
|
||||||
<b-card>
|
|
||||||
<UserScriptPicker :selected-scripts="job.user_scripts"
|
|
||||||
@change="onScriptChange($event)"></UserScriptPicker>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<h4>{{ $t("scanOptions.title") }}</h4>
|
|
||||||
<b-card>
|
|
||||||
<ScanOptions :options="job.scan_options" @change="update()"></ScanOptions>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
</b-card-body>
|
</b-card-body>
|
||||||
|
|
||||||
@ -67,12 +48,10 @@ import ScanOptions from "@/components/ScanOptions";
|
|||||||
import Sist2AdminApi from "@/Sist2AdminApi";
|
import Sist2AdminApi from "@/Sist2AdminApi";
|
||||||
import JobOptions from "@/components/JobOptions";
|
import JobOptions from "@/components/JobOptions";
|
||||||
import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
|
import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
|
||||||
import UserScriptPicker from "@/components/UserScriptPicker.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Job",
|
name: "Job",
|
||||||
components: {
|
components: {
|
||||||
UserScriptPicker,
|
|
||||||
SearchBackendSelect,
|
SearchBackendSelect,
|
||||||
ScanOptions,
|
ScanOptions,
|
||||||
JobOptions
|
JobOptions
|
||||||
@ -81,7 +60,6 @@ export default {
|
|||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
job: null,
|
job: null,
|
||||||
console: console
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -91,8 +69,8 @@ export default {
|
|||||||
update() {
|
update() {
|
||||||
Sist2AdminApi.updateJob(this.getName(), this.job);
|
Sist2AdminApi.updateJob(this.getName(), this.job);
|
||||||
},
|
},
|
||||||
runJob(full = false) {
|
runJob() {
|
||||||
Sist2AdminApi.runJob(this.getName(), full).then(() => {
|
Sist2AdminApi.runJob(this.getName()).then(() => {
|
||||||
this.$bvToast.toast(this.$t("runJobConfirmation"), {
|
this.$bvToast.toast(this.$t("runJobConfirmation"), {
|
||||||
title: this.$t("runJobConfirmationTitle"),
|
title: this.$t("runJobConfirmationTitle"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
@ -117,10 +95,6 @@ export default {
|
|||||||
onBackendSelect(backend) {
|
onBackendSelect(backend) {
|
||||||
this.job.index_options.search_backend = backend;
|
this.job.index_options.search_backend = backend;
|
||||||
this.update();
|
this.update();
|
||||||
},
|
|
||||||
onScriptChange(scripts) {
|
|
||||||
this.job.user_scripts = scripts;
|
|
||||||
this.update();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -44,6 +44,9 @@
|
|||||||
|
|
||||||
<label>{{ $t("backendOptions.batchSize") }}</label>
|
<label>{{ $t("backendOptions.batchSize") }}</label>
|
||||||
<b-form-input v-model="backend.batch_size" type="number" min="1" @change="update()"></b-form-input>
|
<b-form-input v-model="backend.batch_size" type="number" min="1" @change="update()"></b-form-input>
|
||||||
|
|
||||||
|
<label>{{ $t("backendOptions.script") }}</label>
|
||||||
|
<b-form-textarea v-model="backend.script" rows="6" @change="update()"></b-form-textarea>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<label>{{ $t("backendOptions.searchIndex") }}</label>
|
<label>{{ $t("backendOptions.searchIndex") }}</label>
|
||||||
|
@ -92,9 +92,6 @@ export default {
|
|||||||
if ("stderr" in message) {
|
if ("stderr" in message) {
|
||||||
message.level = "ERROR";
|
message.level = "ERROR";
|
||||||
message.message = message["stderr"];
|
message.message = message["stderr"];
|
||||||
} else if ("stdout" in message) {
|
|
||||||
message.level = "INFO";
|
|
||||||
message.message = message["stdout"];
|
|
||||||
} else {
|
} else {
|
||||||
message.level = "ADMIN";
|
message.level = "ADMIN";
|
||||||
message.message = message["sist2-admin"];
|
message.message = message["sist2-admin"];
|
||||||
@ -170,6 +167,6 @@ span.ADMIN {
|
|||||||
margin: 3px;
|
margin: 3px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
color: #000;
|
color: #000;
|
||||||
overflow-y: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -81,7 +81,7 @@ function humanDuration(sec_num) {
|
|||||||
return `${seconds}s`;
|
return `${seconds}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "<1s";
|
return "<0s";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -134,7 +134,7 @@ export default {
|
|||||||
duration: this.taskDuration(row),
|
duration: this.taskDuration(row),
|
||||||
time: moment.utc(row.started).local().format("dd, MMM Do YYYY, HH:mm:ss"),
|
time: moment.utc(row.started).local().format("dd, MMM Do YYYY, HH:mm:ss"),
|
||||||
logs: null,
|
logs: null,
|
||||||
status: row.return_code === 0 ? "ok" : "failed",
|
status: [0,1].includes(row.return_code) ? "ok" : "failed",
|
||||||
_row: row
|
_row: row
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
<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
@ -2,6 +2,4 @@ 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/sist2app/sist2-python.git@2.1
|
|
@ -2,7 +2,6 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from time import sleep
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -19,14 +18,12 @@ 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, Sist2UserScriptTask
|
from jobs import Sist2Job, Sist2ScanTask, TaskQueue, Sist2IndexTask, JobStatus
|
||||||
from notifications import Subscribe, Notifications
|
from notifications import Subscribe, Notifications
|
||||||
from sist2 import Sist2, Sist2SearchBackend
|
from sist2 import Sist2, Sist2SearchBackend
|
||||||
from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION, migrate_v3_to_v4, \
|
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
|
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
|
|
||||||
from util import tail_sync, pid_is_running
|
|
||||||
|
|
||||||
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"))
|
||||||
@ -55,8 +52,7 @@ 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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -78,7 +74,7 @@ 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"])
|
||||||
|
|
||||||
@ -117,10 +113,12 @@ 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()))
|
||||||
|
|
||||||
@ -152,54 +150,23 @@ def _run_job(job: Sist2Job):
|
|||||||
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)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/job/{name:str}/run")
|
@app.get("/api/job/{name:str}/run")
|
||||||
async def run_job(name: str, full: bool = False):
|
async def run_job(name: str):
|
||||||
job: Sist2Job = db["jobs"][name]
|
job = db["jobs"][name]
|
||||||
if not job:
|
if not job:
|
||||||
raise HTTPException(status_code=404)
|
raise HTTPException(status_code=404)
|
||||||
|
|
||||||
if full:
|
|
||||||
job.do_full_scan = True
|
|
||||||
|
|
||||||
_run_job(job)
|
_run_job(job)
|
||||||
|
|
||||||
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")
|
@app.get("/api/job/{name:str}/logs_to_delete")
|
||||||
async def task_history(n: int, name: str):
|
async def task_history(n: int, name: str):
|
||||||
return get_log_files_to_remove(db, name, n)
|
return get_log_files_to_remove(db, name, n)
|
||||||
@ -227,10 +194,7 @@ async def delete_job(name: str):
|
|||||||
@app.delete("/api/frontend/{name:str}")
|
@app.delete("/api/frontend/{name:str}")
|
||||||
async def delete_frontend(name: str):
|
async def delete_frontend(name: str):
|
||||||
if name in RUNNING_FRONTENDS:
|
if name in RUNNING_FRONTENDS:
|
||||||
try:
|
os.kill(RUNNING_FRONTENDS[name], signal.SIGTERM)
|
||||||
os.kill(RUNNING_FRONTENDS[name], signal.SIGTERM)
|
|
||||||
except ProcessLookupError:
|
|
||||||
pass
|
|
||||||
del RUNNING_FRONTENDS[name]
|
del RUNNING_FRONTENDS[name]
|
||||||
|
|
||||||
frontend = db["frontends"][name]
|
frontend = db["frontends"][name]
|
||||||
@ -275,7 +239,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=not insecure, auth=auth)
|
r = requests.get(es_url, verify=insecure, auth=auth)
|
||||||
except SSLError:
|
except SSLError:
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
@ -326,18 +290,7 @@ def start_frontend_(frontend: Sist2Frontend):
|
|||||||
logger.debug(f"Fetched search backend options for {backend_name}")
|
logger.debug(f"Fetched search backend options for {backend_name}")
|
||||||
|
|
||||||
pid = sist2.web(frontend.web_options, search_backend, frontend.name)
|
pid = sist2.web(frontend.web_options, search_backend, frontend.name)
|
||||||
|
|
||||||
sleep(0.2)
|
|
||||||
if not pid_is_running(pid):
|
|
||||||
frontend_log = frontend.get_log_path(LOG_FOLDER)
|
|
||||||
logger.error(f"Frontend exited too quickly, check {frontend_log} for more details:")
|
|
||||||
for line in tail_sync(frontend.get_log_path(LOG_FOLDER), 3):
|
|
||||||
logger.error(line.strip())
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
RUNNING_FRONTENDS[frontend.name] = pid
|
RUNNING_FRONTENDS[frontend.name] = pid
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/frontend/{name:str}/start")
|
@app.post("/api/frontend/{name:str}/start")
|
||||||
@ -346,12 +299,7 @@ async def start_frontend(name: str):
|
|||||||
if not frontend:
|
if not frontend:
|
||||||
raise HTTPException(status_code=404)
|
raise HTTPException(status_code=404)
|
||||||
|
|
||||||
ok = start_frontend_(frontend)
|
start_frontend_(frontend)
|
||||||
|
|
||||||
if not ok:
|
|
||||||
raise HTTPException(status_code=500)
|
|
||||||
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/frontend/{name:str}/stop")
|
@app.post("/api/frontend/{name:str}/stop")
|
||||||
@ -361,7 +309,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"]:
|
||||||
@ -371,7 +319,7 @@ async def get_frontends():
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/search_backend")
|
@app.get("/api/search_backend/")
|
||||||
async def get_search_backends():
|
async def get_search_backends():
|
||||||
return list(db["search_backends"])
|
return list(db["search_backends"])
|
||||||
|
|
||||||
@ -427,59 +375,6 @@ def create_search_backend(name: str):
|
|||||||
return 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:
|
||||||
|
|
||||||
@ -584,8 +479,7 @@ if __name__ == '__main__':
|
|||||||
migrate_v3_to_v4(db)
|
migrate_v3_to_v4(db)
|
||||||
|
|
||||||
if db["sist2_admin"]["info"]["version"] != DB_SCHEMA_VERSION:
|
if db["sist2_admin"]["info"]["version"] != DB_SCHEMA_VERSION:
|
||||||
raise Exception(f"Incompatible database {db.dbfile}. "
|
raise Exception(f"Incompatible database version for {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)
|
||||||
|
@ -9,11 +9,9 @@ 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")
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
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
|
||||||
@ -23,7 +18,6 @@ from notifications import Notifications
|
|||||||
from sist2 import ScanOptions, IndexOptions, Sist2
|
from sist2 import ScanOptions, IndexOptions, Sist2
|
||||||
from state import RUNNING_FRONTENDS, get_log_files_to_remove, delete_log_file
|
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):
|
||||||
@ -38,8 +32,6 @@ 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
|
||||||
|
|
||||||
@ -120,10 +112,6 @@ class Sist2Task:
|
|||||||
|
|
||||||
logger.info(f"Started task {self.display_name}")
|
logger.info(f"Started task {self.display_name}")
|
||||||
|
|
||||||
def set_pid(self, pid):
|
|
||||||
self.pid = pid
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Sist2ScanTask(Sist2Task):
|
class Sist2ScanTask(Sist2Task):
|
||||||
|
|
||||||
@ -137,10 +125,13 @@ class Sist2ScanTask(Sist2Task):
|
|||||||
else:
|
else:
|
||||||
self.job.scan_options.output = None
|
self.job.scan_options.output = None
|
||||||
|
|
||||||
return_code = sist2.scan(self.job.scan_options, logs_cb=self.log_callback, set_pid_cb=self.set_pid)
|
def set_pid(pid):
|
||||||
|
self.pid = pid
|
||||||
|
|
||||||
|
return_code = sist2.scan(self.job.scan_options, logs_cb=self.log_callback, set_pid_cb=set_pid)
|
||||||
self.ended = datetime.utcnow()
|
self.ended = datetime.utcnow()
|
||||||
|
|
||||||
is_ok = (return_code in (0, 1)) if "debug" in sist2.bin_path else (return_code == 0)
|
is_ok = return_code in (0, 1)
|
||||||
|
|
||||||
if not is_ok:
|
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})"}))
|
||||||
@ -166,9 +157,6 @@ class Sist2ScanTask(Sist2Task):
|
|||||||
self.job.previous_index_path = self.job.index_path
|
self.job.previous_index_path = self.job.index_path
|
||||||
db["jobs"][self.job.name] = self.job
|
db["jobs"][self.job.name] = self.job
|
||||||
|
|
||||||
if is_ok:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
return return_code
|
return return_code
|
||||||
|
|
||||||
|
|
||||||
@ -189,12 +177,12 @@ class Sist2IndexTask(Sist2Task):
|
|||||||
|
|
||||||
logger.debug(f"Fetched search backend options for {self.job.index_options.search_backend}")
|
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, set_pid_cb=self.set_pid)
|
return_code = sist2.index(self.job.index_options, search_backend, logs_cb=self.log_callback)
|
||||||
self.ended = datetime.utcnow()
|
self.ended = datetime.utcnow()
|
||||||
|
|
||||||
duration = self.ended - self.started
|
duration = self.ended - self.started
|
||||||
|
|
||||||
ok = return_code in (0, 1)
|
ok = return_code == 0
|
||||||
|
|
||||||
if ok:
|
if ok:
|
||||||
self.restart_running_frontends(db, sist2)
|
self.restart_running_frontends(db, sist2)
|
||||||
@ -204,7 +192,7 @@ class Sist2IndexTask(Sist2Task):
|
|||||||
self.job.previous_index_path = self.job.index_path
|
self.job.previous_index_path = self.job.index_path
|
||||||
db["jobs"][self.job.name] = self.job
|
db["jobs"][self.job.name] = self.job
|
||||||
|
|
||||||
self._logger.info(json.dumps({"sist2-admin": f"Sist2Scan task finished {return_code=}, {duration=}, {ok=}"}))
|
self._logger.info(json.dumps({"sist2-admin": f"Sist2Scan task finished {return_code=}, {duration=}"}))
|
||||||
|
|
||||||
logger.info(f"Completed {self.display_name} ({return_code=})")
|
logger.info(f"Completed {self.display_name} ({return_code=})")
|
||||||
|
|
||||||
@ -243,65 +231,6 @@ class Sist2IndexTask(Sist2Task):
|
|||||||
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, self.set_pid)
|
|
||||||
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.set_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()
|
||||||
@ -320,7 +249,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
|
||||||
|
@ -1,130 +0,0 @@
|
|||||||
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, set_pid_cb):
|
|
||||||
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)
|
|
||||||
proc = subprocess.Popen([setup_script], cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
||||||
set_pid_cb(proc.pid)
|
|
||||||
proc.wait()
|
|
||||||
stdout = proc.stdout.read()
|
|
||||||
|
|
||||||
for line in stdout.split(b"\n"):
|
|
||||||
if line:
|
|
||||||
log_cb({"stdout": line.decode()})
|
|
||||||
|
|
||||||
log_cb({"stdout": f"Executed setup script {setup_script}, return code = {proc.returncode}"})
|
|
||||||
|
|
||||||
if proc.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, set_pid_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, set_pid_cb)
|
|
||||||
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/sist2app/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=""
|
|
||||||
)
|
|
||||||
}
|
|
@ -2,11 +2,10 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from io import TextIOWrapper
|
from io import TextIOWrapper
|
||||||
from logging import FileHandler, StreamHandler
|
from logging import FileHandler
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@ -42,6 +41,8 @@ class Sist2SearchBackend(BaseModel):
|
|||||||
es_insecure_ssl: bool = False
|
es_insecure_ssl: bool = False
|
||||||
es_index: str = "sist2"
|
es_index: str = "sist2"
|
||||||
threads: int = 1
|
threads: int = 1
|
||||||
|
script: str = ""
|
||||||
|
script_file: str = None
|
||||||
batch_size: int = 70
|
batch_size: int = 70
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -73,6 +74,8 @@ class IndexOptions(BaseModel):
|
|||||||
f"--es-index={search_backend.es_index}",
|
f"--es-index={search_backend.es_index}",
|
||||||
f"--batch-size={search_backend.batch_size}"]
|
f"--batch-size={search_backend.batch_size}"]
|
||||||
|
|
||||||
|
if search_backend.script_file:
|
||||||
|
args.append(f"--script-file={search_backend.script_file}")
|
||||||
if search_backend.es_insecure_ssl:
|
if search_backend.es_insecure_ssl:
|
||||||
args.append(f"--es-insecure-ssl")
|
args.append(f"--es-insecure-ssl")
|
||||||
if self.incremental_index:
|
if self.incremental_index:
|
||||||
@ -201,7 +204,6 @@ class WebOptions(BaseModel):
|
|||||||
auth0_client_id: str = None
|
auth0_client_id: str = None
|
||||||
auth0_public_key: str = None
|
auth0_public_key: str = None
|
||||||
auth0_public_key_file: str = None
|
auth0_public_key_file: str = None
|
||||||
verbose: bool = False
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@ -233,8 +235,6 @@ class WebOptions(BaseModel):
|
|||||||
args.append(f"--tag-auth={self.tag_auth}")
|
args.append(f"--tag-auth={self.tag_auth}")
|
||||||
if self.dev:
|
if self.dev:
|
||||||
args.append(f"--dev")
|
args.append(f"--dev")
|
||||||
if self.verbose:
|
|
||||||
args.append(f"--very-verbose")
|
|
||||||
|
|
||||||
args.extend(self.indices)
|
args.extend(self.indices)
|
||||||
|
|
||||||
@ -247,7 +247,14 @@ class Sist2:
|
|||||||
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, search_backend: Sist2SearchBackend, logs_cb, set_pid_cb):
|
def index(self, options: IndexOptions, search_backend: Sist2SearchBackend, logs_cb):
|
||||||
|
|
||||||
|
if search_backend.script and search_backend.backend_type == SearchBackendType("elasticsearch"):
|
||||||
|
with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".painless", delete=False) as f:
|
||||||
|
f.write(search_backend.script)
|
||||||
|
search_backend.script_file = f.name
|
||||||
|
else:
|
||||||
|
search_backend.script_file = None
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
self.bin_path,
|
self.bin_path,
|
||||||
@ -259,9 +266,7 @@ class Sist2:
|
|||||||
logs_cb({"sist2-admin": f"Starting sist2 command with args {args}"})
|
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)
|
||||||
|
|
||||||
set_pid_cb(proc.pid)
|
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
|
||||||
|
|
||||||
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, None, proc))
|
|
||||||
t_stderr.start()
|
t_stderr.start()
|
||||||
|
|
||||||
self._consume_logs_stdout(logs_cb, proc)
|
self._consume_logs_stdout(logs_cb, proc)
|
||||||
@ -288,7 +293,7 @@ class Sist2:
|
|||||||
|
|
||||||
set_pid_cb(proc.pid)
|
set_pid_cb(proc.pid)
|
||||||
|
|
||||||
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, None, proc))
|
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
|
||||||
t_stderr.start()
|
t_stderr.start()
|
||||||
|
|
||||||
self._consume_logs_stdout(logs_cb, proc)
|
self._consume_logs_stdout(logs_cb, proc)
|
||||||
@ -298,7 +303,7 @@ class Sist2:
|
|||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _consume_logs_stderr(logs_cb, exit_cb, proc):
|
def _consume_logs_stderr(logs_cb, proc):
|
||||||
pipe_wrapper = TextIOWrapper(proc.stderr, encoding="utf8", errors="ignore")
|
pipe_wrapper = TextIOWrapper(proc.stderr, encoding="utf8", errors="ignore")
|
||||||
try:
|
try:
|
||||||
for line in pipe_wrapper:
|
for line in pipe_wrapper:
|
||||||
@ -306,9 +311,7 @@ class Sist2:
|
|||||||
continue
|
continue
|
||||||
logs_cb({"stderr": line})
|
logs_cb({"stderr": line})
|
||||||
finally:
|
finally:
|
||||||
return_code = proc.wait()
|
proc.wait()
|
||||||
if exit_cb:
|
|
||||||
exit_cb(return_code)
|
|
||||||
pipe_wrapper.close()
|
pipe_wrapper.close()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -342,19 +345,15 @@ class Sist2:
|
|||||||
|
|
||||||
web_logger = logging.Logger(name=f"sist2-frontend-{name}")
|
web_logger = logging.Logger(name=f"sist2-frontend-{name}")
|
||||||
web_logger.addHandler(FileHandler(os.path.join(LOG_FOLDER, f"frontend-{name}.log")))
|
web_logger.addHandler(FileHandler(os.path.join(LOG_FOLDER, f"frontend-{name}.log")))
|
||||||
web_logger.addHandler(StreamHandler())
|
|
||||||
|
|
||||||
def logs_cb(message):
|
def logs_cb(message):
|
||||||
web_logger.info(json.dumps(message))
|
web_logger.info(json.dumps(message))
|
||||||
|
|
||||||
def exit_cb(return_code):
|
|
||||||
logger.info(f"Web frontend exited with return code {return_code}")
|
|
||||||
|
|
||||||
logger.info(f"Starting frontend {' '.join(args)}")
|
logger.info(f"Starting frontend {' '.join(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, exit_cb, proc))
|
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
|
||||||
t_stderr.start()
|
t_stderr.start()
|
||||||
|
|
||||||
t_stdout = Thread(target=self._consume_logs_stdout, args=(logs_cb, proc))
|
t_stdout = Thread(target=self._consume_logs_stdout, args=(logs_cb, proc))
|
||||||
|
@ -14,7 +14,7 @@ RUNNING_FRONTENDS: Dict[str, int] = {}
|
|||||||
|
|
||||||
TESSERACT_LANGS = get_tesseract_langs()
|
TESSERACT_LANGS = get_tesseract_langs()
|
||||||
|
|
||||||
DB_SCHEMA_VERSION = "5"
|
DB_SCHEMA_VERSION = "4"
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
from glob import glob
|
|
||||||
import os
|
|
||||||
from config import DATA_FOLDER
|
|
||||||
|
|
||||||
|
|
||||||
def get_old_index_files(name):
|
|
||||||
files = glob(os.path.join(DATA_FOLDER, f"scan-{name.replace('/', '_')}-*.sist2"))
|
|
||||||
files = list(sorted(files, key=lambda f: os.stat(f).st_mtime))
|
|
||||||
files = files[-1:]
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
|
|
||||||
def tail_sync(filename, lines=1, _buffer=4098):
|
|
||||||
with open(filename) as f:
|
|
||||||
lines_found = []
|
|
||||||
|
|
||||||
block_counter = -1
|
|
||||||
|
|
||||||
while len(lines_found) < lines:
|
|
||||||
try:
|
|
||||||
f.seek(block_counter * _buffer, os.SEEK_END)
|
|
||||||
except IOError:
|
|
||||||
f.seek(0)
|
|
||||||
lines_found = f.readlines()
|
|
||||||
break
|
|
||||||
|
|
||||||
lines_found = f.readlines()
|
|
||||||
|
|
||||||
block_counter -= 1
|
|
||||||
|
|
||||||
return lines_found[-lines:]
|
|
||||||
|
|
||||||
|
|
||||||
def pid_is_running(pid):
|
|
||||||
try:
|
|
||||||
os.kill(pid, 0)
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
Binary file not shown.
15841
sist2-vue/package-lock.json
generated
15841
sist2-vue/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sist2",
|
"name": "sist2",
|
||||||
"version": "1.0.0",
|
"version": "2.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@ -9,7 +9,8 @@
|
|||||||
"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",
|
||||||
"axios": "^1.6.0",
|
"@tensorflow/tfjs": "^4.4.0",
|
||||||
|
"axios": "^0.25.0",
|
||||||
"bootstrap-vue": "^2.21.2",
|
"bootstrap-vue": "^2.21.2",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"d3": "^5.6.1",
|
"d3": "^5.6.1",
|
||||||
@ -17,7 +18,6 @@
|
|||||||
"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,9 +29,9 @@
|
|||||||
},
|
},
|
||||||
"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-vuex": "~5.0.8",
|
"@vue/cli-plugin-vuex": "~5.0.8",
|
||||||
"@vue/cli-service": "^5.0.8",
|
"@vue/cli-service": "^5.0.8",
|
||||||
"@vue/test-utils": "^1.0.3",
|
"@vue/test-utils": "^1.0.3",
|
||||||
@ -43,7 +43,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",
|
||||||
"vue-cli-plugin-bootstrap-vue": "~0.8.2",
|
"typescript": "~4.1.5",
|
||||||
|
"vue-cli-plugin-bootstrap-vue": "~0.7.0",
|
||||||
"vue-template-compiler": "^2.6.11"
|
"vue-template-compiler": "^2.6.11"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
@ -308,21 +308,15 @@ html, body {
|
|||||||
|
|
||||||
.info-icon {
|
.info-icon {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
min-width: 1rem;
|
|
||||||
margin-right: 0.2rem;
|
margin-right: 0.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
min-height: 1rem;
|
|
||||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==);
|
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==);
|
||||||
filter: brightness(45%);
|
filter: brightness(45%);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-black .info-icon {
|
|
||||||
filter: brightness(80%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,115 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {strUnescape, lum, sid} from "./util";
|
import {ext, strUnescape, lum} from "./util";
|
||||||
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
|
|
||||||
|
export interface EsTag {
|
||||||
|
id: string
|
||||||
|
count: number
|
||||||
|
color: string | undefined
|
||||||
|
isLeaf: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
style: string
|
||||||
|
text: string
|
||||||
|
rawText: string
|
||||||
|
fg: string
|
||||||
|
bg: string
|
||||||
|
userTag: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Index {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
id: string
|
||||||
|
idPrefix: string
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EsHit {
|
||||||
|
_index: string
|
||||||
|
_id: string
|
||||||
|
_score: number
|
||||||
|
_type: string
|
||||||
|
_tags: Tag[]
|
||||||
|
_seq: number
|
||||||
|
_source: {
|
||||||
|
path: string
|
||||||
|
size: number
|
||||||
|
mime: string
|
||||||
|
name: string
|
||||||
|
extension: string
|
||||||
|
index: string
|
||||||
|
_depth: number
|
||||||
|
mtime: number
|
||||||
|
videoc: string
|
||||||
|
audioc: string
|
||||||
|
parent: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
duration: number
|
||||||
|
tag: string[]
|
||||||
|
checksum: string
|
||||||
|
thumbnail: string
|
||||||
|
}
|
||||||
|
_props: {
|
||||||
|
isSubDocument: boolean
|
||||||
|
isImage: boolean
|
||||||
|
isGif: boolean
|
||||||
|
isVideo: boolean
|
||||||
|
isPlayableVideo: boolean
|
||||||
|
isPlayableImage: boolean
|
||||||
|
isAudio: boolean
|
||||||
|
hasThumbnail: boolean
|
||||||
|
hasVidPreview: boolean
|
||||||
|
imageAspectRatio: number
|
||||||
|
/** Number of thumbnails available */
|
||||||
|
tnNum: number
|
||||||
|
}
|
||||||
|
highlight: {
|
||||||
|
name: string[] | undefined,
|
||||||
|
content: string[] | undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdPrefix(indices: Index[], id: string): string {
|
||||||
|
for (let i = 4; i < 32; i++) {
|
||||||
|
const prefix = id.slice(0, i);
|
||||||
|
|
||||||
|
if (indices.filter(idx => idx.id.slice(0, i) == prefix).length == 1) {
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EsResult {
|
||||||
|
took: number
|
||||||
|
|
||||||
|
hits: {
|
||||||
|
// TODO: ES 6.X ?
|
||||||
|
total: {
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
hits: EsHit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregations: any
|
||||||
|
}
|
||||||
|
|
||||||
class Sist2Api {
|
class Sist2Api {
|
||||||
|
|
||||||
baseUrl;
|
private readonly baseUrl: string
|
||||||
sist2Info;
|
private sist2Info: any
|
||||||
queryfunc;
|
private queryfunc: () => EsResult;
|
||||||
|
|
||||||
constructor(baseUrl) {
|
constructor(baseUrl: string) {
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
init(queryFunc) {
|
init(queryFunc: () => EsResult) {
|
||||||
this.queryfunc = queryFunc;
|
this.queryfunc = queryFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,25 +117,28 @@ class Sist2Api {
|
|||||||
return this.sist2Info.searchBackend;
|
return this.sist2Info.searchBackend;
|
||||||
}
|
}
|
||||||
|
|
||||||
models() {
|
getSist2Info(): Promise<any> {
|
||||||
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() {
|
|
||||||
return axios.get(`${this.baseUrl}i`).then(resp => {
|
return axios.get(`${this.baseUrl}i`).then(resp => {
|
||||||
|
const indices = resp.data.indices as Index[];
|
||||||
|
|
||||||
|
resp.data.indices = indices.map(idx => {
|
||||||
|
return {
|
||||||
|
id: idx.id,
|
||||||
|
name: idx.name,
|
||||||
|
timestamp: idx.timestamp,
|
||||||
|
version: idx.version,
|
||||||
|
idPrefix: getIdPrefix(indices, idx.id)
|
||||||
|
} as Index;
|
||||||
|
});
|
||||||
|
|
||||||
this.sist2Info = resp.data;
|
this.sist2Info = resp.data;
|
||||||
|
|
||||||
return resp.data;
|
return resp.data;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setHitProps(hit) {
|
setHitProps(hit: EsHit): void {
|
||||||
hit["_props"] = {};
|
hit["_props"] = {} as any;
|
||||||
|
|
||||||
const mimeCategory = hit._source.mime == null ? null : hit._source.mime.split("/")[0];
|
const mimeCategory = hit._source.mime == null ? null : hit._source.mime.split("/")[0];
|
||||||
|
|
||||||
@ -48,7 +146,7 @@ class Sist2Api {
|
|||||||
hit._props.isSubDocument = true;
|
hit._props.isSubDocument = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("thumbnail" in hit._source && hit._source.thumbnail > 0) {
|
if ("thumbnail" in hit._source) {
|
||||||
hit._props.hasThumbnail = true;
|
hit._props.hasThumbnail = true;
|
||||||
|
|
||||||
if (Number.isNaN(Number(hit._source.thumbnail))) {
|
if (Number.isNaN(Number(hit._source.thumbnail))) {
|
||||||
@ -104,8 +202,8 @@ class Sist2Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setHitTags(hit) {
|
setHitTags(hit: EsHit): void {
|
||||||
const tags = [];
|
const tags = [] as Tag[];
|
||||||
|
|
||||||
// User tags
|
// User tags
|
||||||
if ("tag" in hit._source) {
|
if ("tag" in hit._source) {
|
||||||
@ -117,10 +215,10 @@ class Sist2Api {
|
|||||||
hit._tags = tags;
|
hit._tags = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
createUserTag(tag) {
|
createUserTag(tag: string): Tag {
|
||||||
const tokens = tag.split(".");
|
const tokens = tag.split(".");
|
||||||
|
|
||||||
const colorToken = tokens.pop();
|
const colorToken = tokens.pop() as string;
|
||||||
|
|
||||||
const bg = colorToken;
|
const bg = colorToken;
|
||||||
const fg = lum(colorToken) > 50 ? "#000" : "#fff";
|
const fg = lum(colorToken) > 50 ? "#000" : "#fff";
|
||||||
@ -132,30 +230,25 @@ class Sist2Api {
|
|||||||
text: tokens.join("."),
|
text: tokens.join("."),
|
||||||
rawText: tag,
|
rawText: tag,
|
||||||
userTag: true,
|
userTag: true,
|
||||||
};
|
} as Tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
search() {
|
search(): Promise<EsResult> {
|
||||||
if (this.backend() === "sqlite") {
|
if (this.backend() == "sqlite") {
|
||||||
return this.ftsQuery(this.queryfunc())
|
return this.ftsQuery(this.queryfunc())
|
||||||
} else {
|
} else {
|
||||||
return this.esQuery(this.queryfunc());
|
return this.esQuery(this.queryfunc());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getIndexRoot(indexId) {
|
esQuery(query: any): Promise<EsResult> {
|
||||||
return this.sist2Info.indices.find(idx => idx.id === indexId).root;
|
|
||||||
}
|
|
||||||
|
|
||||||
esQuery(query) {
|
|
||||||
return axios.post(`${this.baseUrl}es`, query).then(resp => {
|
return axios.post(`${this.baseUrl}es`, query).then(resp => {
|
||||||
const res = resp.data;
|
const res = resp.data as EsResult;
|
||||||
|
|
||||||
if (res.hits?.hits) {
|
if (res.hits?.hits) {
|
||||||
res.hits.hits.forEach((hit) => {
|
res.hits.hits.forEach((hit: EsHit) => {
|
||||||
hit["_source"]["name"] = strUnescape(hit["_source"]["name"]);
|
hit["_source"]["name"] = strUnescape(hit["_source"]["name"]);
|
||||||
hit["_source"]["path"] = strUnescape(hit["_source"]["path"]);
|
hit["_source"]["path"] = strUnescape(hit["_source"]["path"]);
|
||||||
hit["_source"]["indexRoot"] = this._getIndexRoot(hit["_source"]["index"]);
|
|
||||||
|
|
||||||
this.setHitProps(hit);
|
this.setHitProps(hit);
|
||||||
this.setHitTags(hit);
|
this.setHitTags(hit);
|
||||||
@ -166,9 +259,9 @@ class Sist2Api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ftsQuery(query) {
|
ftsQuery(query: any): Promise<EsResult> {
|
||||||
return axios.post(`${this.baseUrl}fts/search`, query).then(resp => {
|
return axios.post(`${this.baseUrl}fts/search`, query).then(resp => {
|
||||||
const res = resp.data;
|
const res = resp.data as any;
|
||||||
|
|
||||||
if (res.hits.hits) {
|
if (res.hits.hits) {
|
||||||
res.hits.hits.forEach(hit => {
|
res.hits.hits.forEach(hit => {
|
||||||
@ -189,7 +282,7 @@ class Sist2Api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMimeTypesEs(query) {
|
private getMimeTypesEs(query) {
|
||||||
const AGGS = {
|
const AGGS = {
|
||||||
mimeTypes: {
|
mimeTypes: {
|
||||||
terms: {
|
terms: {
|
||||||
@ -218,7 +311,7 @@ class Sist2Api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMimeTypesSqlite() {
|
private getMimeTypesSqlite(): Promise<[{ mime: string, count: number }]> {
|
||||||
return axios.get(`${this.baseUrl}fts/mimetypes`)
|
return axios.get(`${this.baseUrl}fts/mimetypes`)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
return resp.data;
|
return resp.data;
|
||||||
@ -228,15 +321,15 @@ class Sist2Api {
|
|||||||
async getMimeTypes(query = undefined) {
|
async getMimeTypes(query = undefined) {
|
||||||
let buckets;
|
let buckets;
|
||||||
|
|
||||||
if (this.backend() === "sqlite") {
|
if (this.backend() == "sqlite") {
|
||||||
buckets = await this.getMimeTypesSqlite();
|
buckets = await this.getMimeTypesSqlite();
|
||||||
} else {
|
} else {
|
||||||
buckets = await this.getMimeTypesEs(query);
|
buckets = await this.getMimeTypesEs(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mimeMap = [];
|
const mimeMap: any[] = [];
|
||||||
|
|
||||||
buckets.sort((a, b) => a.mime > b.mime).forEach((bucket) => {
|
buckets.sort((a: any, b: any) => a.mime > b.mime).forEach((bucket: any) => {
|
||||||
const tmp = bucket.mime.split("/");
|
const tmp = bucket.mime.split("/");
|
||||||
const category = tmp[0];
|
const category = tmp[0];
|
||||||
const mime = tmp[1];
|
const mime = tmp[1];
|
||||||
@ -270,7 +363,7 @@ class Sist2Api {
|
|||||||
return {buckets, mimeMap};
|
return {buckets, mimeMap};
|
||||||
}
|
}
|
||||||
|
|
||||||
_createEsTag(tag, count) {
|
_createEsTag(tag: string, count: number): EsTag {
|
||||||
const tokens = tag.split(".");
|
const tokens = tag.split(".");
|
||||||
|
|
||||||
if (/.*\.#[0-9a-fA-F]{6}/.test(tag)) {
|
if (/.*\.#[0-9a-fA-F]{6}/.test(tag)) {
|
||||||
@ -290,7 +383,7 @@ class Sist2Api {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagsEs() {
|
private getTagsEs() {
|
||||||
return this.esQuery({
|
return this.esQuery({
|
||||||
aggs: {
|
aggs: {
|
||||||
tags: {
|
tags: {
|
||||||
@ -303,21 +396,21 @@ class Sist2Api {
|
|||||||
size: 0,
|
size: 0,
|
||||||
}).then(resp => {
|
}).then(resp => {
|
||||||
return resp["aggregations"]["tags"]["buckets"]
|
return resp["aggregations"]["tags"]["buckets"]
|
||||||
.sort((a, b) => a["key"].localeCompare(b["key"]))
|
.sort((a: any, b: any) => a["key"].localeCompare(b["key"]))
|
||||||
.map((bucket) => this._createEsTag(bucket["key"], bucket["doc_count"]));
|
.map((bucket: any) => this._createEsTag(bucket["key"], bucket["doc_count"]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagsSqlite() {
|
private getTagsSqlite() {
|
||||||
return axios.get(`${this.baseUrl}fts/tags`)
|
return axios.get(`${this.baseUrl}/fts/tags`)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
return resp.data.map(tag => this._createEsTag(tag.tag, tag.count))
|
return resp.data.map(tag => this._createEsTag(tag.tag, tag.count))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTags() {
|
async getTags(): Promise<EsTag[]> {
|
||||||
let tags;
|
let tags;
|
||||||
if (this.backend() === "sqlite") {
|
if (this.backend() == "sqlite") {
|
||||||
tags = await this.getTagsSqlite();
|
tags = await this.getTagsSqlite();
|
||||||
} else {
|
} else {
|
||||||
tags = await this.getTagsEs();
|
tags = await this.getTagsEs();
|
||||||
@ -326,7 +419,7 @@ class Sist2Api {
|
|||||||
// Remove duplicates (same tag with different color)
|
// Remove duplicates (same tag with different color)
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
||||||
return tags.filter((t) => {
|
return tags.filter((t: EsTag) => {
|
||||||
if (seen.has(t.id)) {
|
if (seen.has(t.id)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -335,29 +428,31 @@ class Sist2Api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saveTag(tag, hit) {
|
saveTag(tag: string, hit: EsHit) {
|
||||||
return axios.post(`${this.baseUrl}tag/${sid(hit)}`, {
|
return axios.post(`${this.baseUrl}tag/` + hit["_source"]["index"], {
|
||||||
delete: false,
|
delete: false,
|
||||||
name: tag,
|
name: tag,
|
||||||
|
doc_id: hit["_id"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTag(tag, hit) {
|
deleteTag(tag: string, hit: EsHit) {
|
||||||
return axios.post(`${this.baseUrl}tag/${sid(hit)}`, {
|
return axios.post(`${this.baseUrl}tag/` + hit["_source"]["index"], {
|
||||||
delete: true,
|
delete: true,
|
||||||
name: tag,
|
name: tag,
|
||||||
|
doc_id: hit["_id"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
searchPaths(indexId, minDepth, maxDepth, prefix = null) {
|
searchPaths(indexId, minDepth, maxDepth, prefix = null) {
|
||||||
if (this.backend() === "sqlite") {
|
if (this.backend() == "sqlite") {
|
||||||
return this.searchPathsSqlite(indexId, minDepth, minDepth, prefix);
|
return this.searchPathsSqlite(indexId, minDepth, minDepth, prefix);
|
||||||
} else {
|
} else {
|
||||||
return this.searchPathsEs(indexId, minDepth, maxDepth, prefix);
|
return this.searchPathsEs(indexId, minDepth, maxDepth, prefix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchPathsSqlite(indexId, minDepth, maxDepth, prefix) {
|
private searchPathsSqlite(indexId, minDepth, maxDepth, prefix) {
|
||||||
return axios.post(`${this.baseUrl}fts/paths`, {
|
return axios.post(`${this.baseUrl}fts/paths`, {
|
||||||
indexId, minDepth, maxDepth, prefix
|
indexId, minDepth, maxDepth, prefix
|
||||||
}).then(resp => {
|
}).then(resp => {
|
||||||
@ -365,7 +460,7 @@ class Sist2Api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
searchPathsEs(indexId, minDepth, maxDepth, prefix) {
|
private searchPathsEs(indexId, minDepth, maxDepth, prefix): Promise<[{ path: string, count: number }]> {
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
query: {
|
query: {
|
||||||
@ -410,25 +505,23 @@ class Sist2Api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDateRangeSqlite() {
|
private getDateRangeSqlite() {
|
||||||
return axios.get(`${this.baseUrl}fts/dateRange`)
|
return axios.get(`${this.baseUrl}fts/dateRange`)
|
||||||
.then(resp => ({
|
.then(resp => ({
|
||||||
min: resp.data.dateMin,
|
min: resp.data.dateMin,
|
||||||
max: (resp.data.dateMax === resp.data.dateMin)
|
max: resp.data.dateMax,
|
||||||
? resp.data.dateMax + 1
|
|
||||||
: resp.data.dateMax,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getDateRange() {
|
getDateRange(): Promise<{ min: number, max: number }> {
|
||||||
if (this.backend() === "sqlite") {
|
if (this.backend() == "sqlite") {
|
||||||
return this.getDateRangeSqlite();
|
return this.getDateRangeSqlite();
|
||||||
} else {
|
} else {
|
||||||
return this.getDateRangeEs();
|
return this.getDateRangeEs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDateRangeEs() {
|
private getDateRangeEs() {
|
||||||
return this.esQuery({
|
return this.esQuery({
|
||||||
// TODO: filter current selected indices
|
// TODO: filter current selected indices
|
||||||
aggs: {
|
aggs: {
|
||||||
@ -445,7 +538,7 @@ class Sist2Api {
|
|||||||
if (range.min == null) {
|
if (range.min == null) {
|
||||||
range.min = 0;
|
range.min = 0;
|
||||||
range.max = 1;
|
range.max = 1;
|
||||||
} else if (range.min === range.max) {
|
} else if (range.min == range.max) {
|
||||||
range.max += 1;
|
range.max += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,7 +546,7 @@ class Sist2Api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPathSuggestionsSqlite(text) {
|
private getPathSuggestionsSqlite(text: string) {
|
||||||
return axios.post(`${this.baseUrl}fts/paths`, {
|
return axios.post(`${this.baseUrl}fts/paths`, {
|
||||||
prefix: text,
|
prefix: text,
|
||||||
minDepth: 1,
|
minDepth: 1,
|
||||||
@ -463,7 +556,7 @@ class Sist2Api {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getPathSuggestionsEs(text) {
|
private getPathSuggestionsEs(text) {
|
||||||
return this.esQuery({
|
return this.esQuery({
|
||||||
suggest: {
|
suggest: {
|
||||||
path: {
|
path: {
|
||||||
@ -481,31 +574,31 @@ class Sist2Api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPathSuggestions(text) {
|
getPathSuggestions(text: string): Promise<string[]> {
|
||||||
if (this.backend() === "sqlite") {
|
if (this.backend() == "sqlite") {
|
||||||
return this.getPathSuggestionsSqlite(text);
|
return this.getPathSuggestionsSqlite(text);
|
||||||
} else {
|
} else {
|
||||||
return this.getPathSuggestionsEs(text)
|
return this.getPathSuggestionsEs(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTreemapStat(indexId) {
|
getTreemapStat(indexId: string) {
|
||||||
return `${this.baseUrl}s/${indexId}/TMAP`;
|
return `${this.baseUrl}s/${indexId}/TMAP`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMimeStat(indexId) {
|
getMimeStat(indexId: string) {
|
||||||
return `${this.baseUrl}s/${indexId}/MAGG`;
|
return `${this.baseUrl}s/${indexId}/MAGG`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSizeStat(indexId) {
|
getSizeStat(indexId: string) {
|
||||||
return `${this.baseUrl}s/${indexId}/SAGG`;
|
return `${this.baseUrl}s/${indexId}/SAGG`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDateStat(indexId) {
|
getDateStat(indexId: string) {
|
||||||
return `${this.baseUrl}s/${indexId}/DAGG`;
|
return `${this.baseUrl}s/${indexId}/DAGG`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocumentEs(sid, highlight, fuzzy) {
|
private getDocumentEs(docId: string, highlight: boolean, fuzzy: boolean) {
|
||||||
const query = Sist2Query.searchQuery();
|
const query = Sist2Query.searchQuery();
|
||||||
|
|
||||||
if (highlight) {
|
if (highlight) {
|
||||||
@ -525,15 +618,6 @@ class Sist2Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("knn" in query) {
|
|
||||||
query.query = {
|
|
||||||
bool: {
|
|
||||||
must: []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
delete query.knn;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("function_score" in query.query) {
|
if ("function_score" in query.query) {
|
||||||
query.query = query.query.function_score.query;
|
query.query = query.query.function_score.query;
|
||||||
}
|
}
|
||||||
@ -544,7 +628,7 @@ class Sist2Api {
|
|||||||
query.query.bool.must = [query.query.bool.must];
|
query.query.bool.must = [query.query.bool.must];
|
||||||
}
|
}
|
||||||
|
|
||||||
query.query.bool.must.push({match: {_id: sid}});
|
query.query.bool.must.push({match: {_id: docId}});
|
||||||
|
|
||||||
delete query["sort"];
|
delete query["sort"];
|
||||||
delete query["aggs"];
|
delete query["aggs"];
|
||||||
@ -565,35 +649,35 @@ class Sist2Api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocumentSqlite(sid) {
|
private getDocumentSqlite(docId: string): Promise<EsHit> {
|
||||||
return axios.get(`${this.baseUrl}fts/d/${sid}`)
|
return axios.get(`${this.baseUrl}/fts/d/${docId}`)
|
||||||
.then(resp => ({
|
.then(resp => ({
|
||||||
_source: resp.data
|
_source: resp.data
|
||||||
}));
|
} as EsHit));
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocument(sid, highlight, fuzzy) {
|
getDocument(docId: string, highlight: boolean, fuzzy: boolean): Promise<EsHit | null> {
|
||||||
if (this.backend() === "sqlite") {
|
if (this.backend() == "sqlite") {
|
||||||
return this.getDocumentSqlite(sid);
|
return this.getDocumentSqlite(docId);
|
||||||
} else {
|
} else {
|
||||||
return this.getDocumentEs(sid, highlight, fuzzy);
|
return this.getDocumentEs(docId, highlight, fuzzy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagSuggestions(prefix) {
|
getTagSuggestions(prefix: string): Promise<string[]> {
|
||||||
if (this.backend() === "sqlite") {
|
if (this.backend() == "sqlite") {
|
||||||
return this.getTagSuggestionsSqlite(prefix);
|
return this.getTagSuggestionsSqlite(prefix);
|
||||||
} else {
|
} else {
|
||||||
return this.getTagSuggestionsEs(prefix);
|
return this.getTagSuggestionsEs(prefix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagSuggestionsSqlite(prefix) {
|
private getTagSuggestionsSqlite(prefix): Promise<string[]> {
|
||||||
return axios.post(`${this.baseUrl}fts/suggestTags`, prefix)
|
return axios.post(`${this.baseUrl}/fts/suggestTags`, prefix)
|
||||||
.then(resp => (resp.data));
|
.then(resp => (resp.data));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagSuggestionsEs(prefix) {
|
private getTagSuggestionsEs(prefix): Promise<string[]> {
|
||||||
return this.esQuery({
|
return this.esQuery({
|
||||||
suggest: {
|
suggest: {
|
||||||
tag: {
|
tag: {
|
||||||
@ -618,11 +702,6 @@ class Sist2Api {
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmbeddings(sid, modelId) {
|
|
||||||
return axios.post(`${this.baseUrl}e/${sid}/${modelId.toString().padStart(3, '0')}`)
|
|
||||||
.then(resp => (resp.data));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Sist2Api("");
|
export default new Sist2Api("");
|
@ -1,5 +1,5 @@
|
|||||||
import store from "@/store";
|
import store from "./store";
|
||||||
import Sist2Api from "@/Sist2Api";
|
import {EsHit, Index} from "@/Sist2Api";
|
||||||
|
|
||||||
const SORT_MODES = {
|
const SORT_MODES = {
|
||||||
score: {
|
score: {
|
||||||
@ -7,62 +7,62 @@ const SORT_MODES = {
|
|||||||
{_score: {order: "desc"}},
|
{_score: {order: "desc"}},
|
||||||
{_tie: {order: "asc"}}
|
{_tie: {order: "asc"}}
|
||||||
],
|
],
|
||||||
key: (hit) => hit._score
|
key: (hit: EsHit) => hit._score
|
||||||
},
|
},
|
||||||
random: {
|
random: {
|
||||||
mode: [
|
mode: [
|
||||||
{_score: {order: "desc"}},
|
{_score: {order: "desc"}},
|
||||||
{_tie: {order: "asc"}}
|
{_tie: {order: "asc"}}
|
||||||
],
|
],
|
||||||
key: (hit) => hit._score
|
key: (hit: EsHit) => hit._score
|
||||||
},
|
},
|
||||||
dateAsc: {
|
dateAsc: {
|
||||||
mode: [
|
mode: [
|
||||||
{mtime: {order: "asc"}},
|
{mtime: {order: "asc"}},
|
||||||
{_tie: {order: "asc"}}
|
{_tie: {order: "asc"}}
|
||||||
],
|
],
|
||||||
key: (hit) => hit._source.mtime
|
key: (hit: EsHit) => hit._source.mtime
|
||||||
},
|
},
|
||||||
dateDesc: {
|
dateDesc: {
|
||||||
mode: [
|
mode: [
|
||||||
{mtime: {order: "desc"}},
|
{mtime: {order: "desc"}},
|
||||||
{_tie: {order: "asc"}}
|
{_tie: {order: "asc"}}
|
||||||
],
|
],
|
||||||
key: (hit) => hit._source.mtime
|
key: (hit: EsHit) => hit._source.mtime
|
||||||
},
|
},
|
||||||
sizeAsc: {
|
sizeAsc: {
|
||||||
mode: [
|
mode: [
|
||||||
{size: {order: "asc"}},
|
{size: {order: "asc"}},
|
||||||
{_tie: {order: "asc"}}
|
{_tie: {order: "asc"}}
|
||||||
],
|
],
|
||||||
key: (hit) => hit._source.size
|
key: (hit: EsHit) => hit._source.size
|
||||||
},
|
},
|
||||||
sizeDesc: {
|
sizeDesc: {
|
||||||
mode: [
|
mode: [
|
||||||
{size: {order: "desc"}},
|
{size: {order: "desc"}},
|
||||||
{_tie: {order: "asc"}}
|
{_tie: {order: "asc"}}
|
||||||
],
|
],
|
||||||
key: (hit) => hit._source.size
|
key: (hit: EsHit) => hit._source.size
|
||||||
},
|
},
|
||||||
nameAsc: {
|
nameAsc: {
|
||||||
mode: [
|
mode: [
|
||||||
{name: {order: "asc"}},
|
{name: {order: "asc"}},
|
||||||
{_tie: {order: "asc"}}
|
{_tie: {order: "asc"}}
|
||||||
],
|
],
|
||||||
key: (hit) => hit._source.name
|
key: (hit: EsHit) => hit._source.name
|
||||||
},
|
},
|
||||||
nameDesc: {
|
nameDesc: {
|
||||||
mode: [
|
mode: [
|
||||||
{name: {order: "desc"}},
|
{name: {order: "desc"}},
|
||||||
{_tie: {order: "asc"}}
|
{_tie: {order: "asc"}}
|
||||||
],
|
],
|
||||||
key: (hit) => hit._source.name
|
key: (hit: EsHit) => hit._source.name
|
||||||
}
|
}
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
class Sist2ElasticsearchQuery {
|
class Sist2ElasticsearchQuery {
|
||||||
|
|
||||||
searchQuery(blankSearch = false) {
|
searchQuery(blankSearch: boolean = false): any {
|
||||||
|
|
||||||
const getters = store.getters;
|
const getters = store.getters;
|
||||||
|
|
||||||
@ -76,17 +76,15 @@ class Sist2ElasticsearchQuery {
|
|||||||
const fuzzy = getters.fuzzy;
|
const fuzzy = getters.fuzzy;
|
||||||
const size = getters.size;
|
const size = getters.size;
|
||||||
const after = getters.lastDoc;
|
const after = getters.lastDoc;
|
||||||
const selectedIndexIds = getters.selectedIndices.map((idx) => 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}}
|
||||||
];
|
] as any[];
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
"name^8",
|
"name^8",
|
||||||
@ -117,11 +115,11 @@ class Sist2ElasticsearchQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dateMin && dateMax) {
|
if (dateMin && dateMax) {
|
||||||
filters.push({range: {mtime: {gte: dateMin, lte: dateMax, format: "epoch_second"}}})
|
filters.push({range: {mtime: {gte: dateMin, lte: dateMax}}})
|
||||||
} else if (dateMin) {
|
} else if (dateMin) {
|
||||||
filters.push({range: {mtime: {gte: dateMin, format: "epoch_second"}}})
|
filters.push({range: {mtime: {gte: dateMin}}})
|
||||||
} else if (dateMax) {
|
} else if (dateMax) {
|
||||||
filters.push({range: {mtime: {lte: dateMax, format: "epoch_second"}}})
|
filters.push({range: {mtime: {lte: dateMax}}})
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = pathText.replace(/\/$/, "").toLowerCase(); //remove trailing slashes
|
const path = pathText.replace(/\/$/, "").toLowerCase(); //remove trailing slashes
|
||||||
@ -138,7 +136,7 @@ class Sist2ElasticsearchQuery {
|
|||||||
if (getters.optTagOrOperator) {
|
if (getters.optTagOrOperator) {
|
||||||
filters.push({terms: {"tag": selectedTags}});
|
filters.push({terms: {"tag": selectedTags}});
|
||||||
} else {
|
} else {
|
||||||
selectedTags.forEach((tag) => filters.push({term: {"tag": tag}}));
|
selectedTags.forEach((tag: string) => filters.push({term: {"tag": tag}}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -164,16 +162,16 @@ class Sist2ElasticsearchQuery {
|
|||||||
|
|
||||||
const q = {
|
const q = {
|
||||||
_source: {
|
_source: {
|
||||||
excludes: ["content", "_tie", "emb.*"]
|
excludes: ["content", "_tie"]
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
bool: {
|
bool: {
|
||||||
filter: filters,
|
filter: filters,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sort: SORT_MODES[sortMode].mode,
|
sort: SORT_MODES[getters.sortMode].mode,
|
||||||
size: size,
|
size: size,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
if (!after) {
|
if (!after) {
|
||||||
q.aggs = {
|
q.aggs = {
|
||||||
@ -183,57 +181,14 @@ class Sist2ElasticsearchQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!empty && !blankSearch) {
|
if (!empty && !blankSearch) {
|
||||||
if (getters.embedding) {
|
q.query.bool.must = query;
|
||||||
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[sortMode].key(after), after["_id"]];
|
q.search_after = [SORT_MODES[getters.sortMode].key(after), after["_id"]];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getters.optHighlight && !getters.embedding) {
|
if (getters.optHighlight) {
|
||||||
q.highlight = {
|
q.highlight = {
|
||||||
pre_tags: ["<mark>"],
|
pre_tags: ["<mark>"],
|
||||||
post_tags: ["</mark>"],
|
post_tags: ["</mark>"],
|
||||||
@ -259,7 +214,7 @@ class Sist2ElasticsearchQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortMode === "random") {
|
if (getters.sortMode === "random") {
|
||||||
q.query = {
|
q.query = {
|
||||||
function_score: {
|
function_score: {
|
||||||
query: {
|
query: {
|
@ -1,4 +1,5 @@
|
|||||||
import store from "@/store";
|
import store from "./store";
|
||||||
|
import {EsHit, Index} from "@/Sist2Api";
|
||||||
|
|
||||||
const SORT_MODES = {
|
const SORT_MODES = {
|
||||||
score: {
|
score: {
|
||||||
@ -28,12 +29,18 @@ const SORT_MODES = {
|
|||||||
"sort": "name",
|
"sort": "name",
|
||||||
"sortAsc": false
|
"sortAsc": false
|
||||||
}
|
}
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
|
interface SortMode {
|
||||||
|
text: string
|
||||||
|
mode: any[]
|
||||||
|
key: (hit: EsHit) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Sist2ElasticsearchQuery {
|
class Sist2ElasticsearchQuery {
|
||||||
|
|
||||||
searchQuery() {
|
searchQuery(): any {
|
||||||
|
|
||||||
const getters = store.getters;
|
const getters = store.getters;
|
||||||
|
|
||||||
@ -45,7 +52,7 @@ class Sist2ElasticsearchQuery {
|
|||||||
const dateMax = getters.dateMax;
|
const dateMax = getters.dateMax;
|
||||||
const size = getters.size;
|
const size = getters.size;
|
||||||
const after = getters.lastDoc;
|
const after = getters.lastDoc;
|
||||||
const selectedIndexIds = getters.selectedIndices.map((idx) => 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;
|
||||||
|
|
||||||
@ -88,7 +95,7 @@ class Sist2ElasticsearchQuery {
|
|||||||
if (selectedTags.length > 0) {
|
if (selectedTags.length > 0) {
|
||||||
q["tags"] = selectedTags
|
q["tags"] = selectedTags
|
||||||
}
|
}
|
||||||
if (getters.sortMode === "random") {
|
if (getters.sortMode == "random") {
|
||||||
q["seed"] = getters.seed;
|
q["seed"] = getters.seed;
|
||||||
}
|
}
|
||||||
if (getters.optHighlight) {
|
if (getters.optHighlight) {
|
||||||
@ -96,18 +103,6 @@ class Sist2ElasticsearchQuery {
|
|||||||
q["highlightContextSize"] = Number(getters.optFragmentSize);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
q["searchInPath"] = getters.optSearchInPath;
|
|
||||||
|
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@ export default {
|
|||||||
props: ["span", "text"],
|
props: ["span", "text"],
|
||||||
methods: {
|
methods: {
|
||||||
getStyle() {
|
getStyle() {
|
||||||
return ModelsRepo.data[this.$store.getters.nerModel.name].labelStyles[this.span.label];
|
return ModelsRepo.data[this.$store.getters.mlModel.name].labelStyles[this.span.label];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ export default {
|
|||||||
props: ["spans", "text"],
|
props: ["spans", "text"],
|
||||||
computed: {
|
computed: {
|
||||||
legend() {
|
legend() {
|
||||||
return Object.entries(ModelsRepo.data[this.$store.state.nerModel.name].legend)
|
return Object.entries(ModelsRepo.data[this.$store.state.mlModel.name].legend)
|
||||||
.map(([label, name]) => ({
|
.map(([label, name]) => ({
|
||||||
text: name,
|
text: name,
|
||||||
id: label,
|
id: label,
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import {humanFileSize, burrow} from "@/util";
|
import {burrow} from "@/util-js"
|
||||||
|
import {humanFileSize} from "@/util";
|
||||||
import Sist2Api from "@/Sist2Api";
|
import Sist2Api from "@/Sist2Api";
|
||||||
import domtoimage from "dom-to-image";
|
import domtoimage from "dom-to-image";
|
||||||
|
|
||||||
|
@ -31,7 +31,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import noUiSlider from 'nouislider';
|
import noUiSlider from 'nouislider';
|
||||||
import 'nouislider/dist/nouislider.css';
|
import 'nouislider/dist/nouislider.css';
|
||||||
import {humanDate, mergeTooltips} from "@/util";
|
import {humanDate} from "@/util";
|
||||||
|
import {mergeTooltips} from "@/util-js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "DateSlider",
|
name: "DateSlider",
|
||||||
|
@ -45,8 +45,7 @@ export default {
|
|||||||
items.push(
|
items.push(
|
||||||
{key: "esVersion", value: this.$store.state.sist2Info.esVersion},
|
{key: "esVersion", value: this.$store.state.sist2Info.esVersion},
|
||||||
{key: "esVersionSupported", value: this.$store.state.sist2Info.esVersionSupported},
|
{key: "esVersionSupported", value: this.$store.state.sist2Info.esVersionSupported},
|
||||||
{key: "esVersionLegacy", value: this.$store.state.sist2Info.esVersionLegacy},
|
{key: "esVersionLegacy", value: this.$store.state.sist2Info.esVersionLegacy}
|
||||||
{key: "esVersionHasKnn", value: this.$store.state.sist2Info.esVersionHasKnn},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<!-- 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/${sid(doc)}`"
|
: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">
|
||||||
@ -24,7 +24,6 @@
|
|||||||
<!-- 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>
|
||||||
<MLIcon v-if="doc._source.embedding" clickable @click="onEmbeddingClick()"></MLIcon>
|
|
||||||
<DocFileTitle :doc="doc"></DocFileTitle>
|
<DocFileTitle :doc="doc"></DocFileTitle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -43,19 +42,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {ext, humanFileSize, humanTime, sid} from "@/util";
|
import {ext, humanFileSize, humanTime} from "@/util";
|
||||||
import TagContainer from "@/components/TagContainer.vue";
|
import TagContainer from "@/components/TagContainer.vue";
|
||||||
import DocFileTitle from "@/components/DocFileTitle.vue";
|
import DocFileTitle from "@/components/DocFileTitle.vue";
|
||||||
import DocInfoModal from "@/components/DocInfoModal.vue";
|
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: {MLIcon, FeaturedFieldsLine, FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer},
|
components: {FeaturedFieldsLine, FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer},
|
||||||
props: ["doc", "width"],
|
props: ["doc", "width"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -69,19 +66,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sid: sid,
|
|
||||||
humanFileSize: humanFileSize,
|
humanFileSize: humanFileSize,
|
||||||
humanTime: humanTime,
|
humanTime: humanTime,
|
||||||
onInfoClick() {
|
onInfoClick() {
|
||||||
this.showInfo = true;
|
this.showInfo = true;
|
||||||
},
|
},
|
||||||
onEmbeddingClick() {
|
|
||||||
Sist2Api.getEmbeddings(sid(this.doc), this.$store.state.embeddingsModel).then(embeddings => {
|
|
||||||
this.$store.commit("setEmbeddingText", "");
|
|
||||||
this.$store.commit("setEmbedding", embeddings);
|
|
||||||
this.$store.commit("setEmbeddingDoc", this.doc);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async onThumbnailClick() {
|
async onThumbnailClick() {
|
||||||
this.$store.commit("setUiLightboxSlide", this.doc._seq);
|
this.$store.commit("setUiLightboxSlide", this.doc._seq);
|
||||||
await this.$store.dispatch("showLightbox");
|
await this.$store.dispatch("showLightbox");
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<GridLayout
|
<GridLayout
|
||||||
ref="grid-layout"
|
ref="grid-layout"
|
||||||
:options="gridOptions"
|
:options="gridOptions"
|
||||||
@append="append"
|
@append="append"
|
||||||
@layout-complete="$emit('layout-complete')"
|
@layout-complete="$emit('layout-complete')"
|
||||||
>
|
>
|
||||||
<DocCard v-for="doc in docs" :key="sid(doc)" :doc="doc" :width="width"></DocCard>
|
<DocCard v-for="doc in docs" :key="doc._id" :doc="doc" :width="width"></DocCard>
|
||||||
</GridLayout>
|
</GridLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {sid} from "@/util";
|
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import DocCard from "@/components/DocCard";
|
import DocCard from "@/components/DocCard";
|
||||||
|
|
||||||
@ -19,53 +18,50 @@ import VueInfiniteGrid from "@egjs/vue-infinitegrid";
|
|||||||
Vue.use(VueInfiniteGrid);
|
Vue.use(VueInfiniteGrid);
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
DocCard,
|
DocCard,
|
||||||
},
|
},
|
||||||
props: ["docs", "append"],
|
props: ["docs", "append"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
width: 0,
|
width: 0,
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
align: "center",
|
align: "center",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
transitionDuration: 0,
|
transitionDuration: 0,
|
||||||
isOverflowScroll: false,
|
isOverflowScroll: false,
|
||||||
isConstantSize: false,
|
isConstantSize: false,
|
||||||
useFit: false,
|
useFit: false,
|
||||||
// Indicates whether keep the number of DOMs is maintained. If the useRecycle value is 'true', keep the number
|
// Indicates whether keep the number of DOMs is maintained. If the useRecycle value is 'true', keep the number
|
||||||
// of DOMs is maintained. If the useRecycle value is 'false', the number of DOMs will increase as card elements
|
// of DOMs is maintained. If the useRecycle value is 'false', the number of DOMs will increase as card elements
|
||||||
// are added.
|
// are added.
|
||||||
useRecycle: false
|
useRecycle: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
computed: {
|
||||||
sid,
|
colCount() {
|
||||||
},
|
const columns = this.$store.getters["optColumns"];
|
||||||
computed: {
|
|
||||||
colCount() {
|
|
||||||
const columns = this.$store.getters["optColumns"];
|
|
||||||
|
|
||||||
if (columns === "auto") {
|
if (columns === "auto") {
|
||||||
return Math.round(this.$refs["grid-layout"].$el.scrollWidth / 300)
|
return Math.round(this.$refs["grid-layout"].$el.scrollWidth / 300)
|
||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
},
|
||||||
this.width = this.$refs["grid-layout"].$el.scrollWidth / this.colCount;
|
mounted() {
|
||||||
|
this.width = this.$refs["grid-layout"].$el.scrollWidth / this.colCount;
|
||||||
|
|
||||||
if (this.colCount === 1) {
|
if (this.colCount === 1) {
|
||||||
this.$refs["grid-layout"].$el.classList.add("grid-single-column");
|
this.$refs["grid-layout"].$el.classList.add("grid-single-column");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.subscribe((mutation) => {
|
this.$store.subscribe((mutation) => {
|
||||||
if (mutation.type === "busUpdateWallItems" && this.$refs["grid-layout"]) {
|
if (mutation.type === "busUpdateWallItems" && this.$refs["grid-layout"]) {
|
||||||
this.$refs["grid-layout"].updateItems();
|
this.$refs["grid-layout"].updateItems();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,71 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<a :href="`f/${sid(doc)}`"
|
<a :href="`f/${doc._source.index}/${doc._id}`" class="file-title-anchor" target="_blank">
|
||||||
:class="doc._source.embedding ? 'file-title-anchor-with-embedding' : 'file-title-anchor'" target="_blank">
|
<div class="file-title" :title="doc._source.path + '/' + doc._source.name + ext(doc)"
|
||||||
<div class="file-title" :title="doc._source.path + '/' + doc._source.name + ext(doc)"
|
v-html="fileName() + ext(doc)"></div>
|
||||||
v-html="fileName() + ext(doc)"></div>
|
</a>
|
||||||
</a>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {ext, sid} from "@/util";
|
import {ext} from "@/util";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "DocFileTitle",
|
name: "DocFileTitle",
|
||||||
props: ["doc"],
|
props: ["doc"],
|
||||||
methods: {
|
methods: {
|
||||||
sid: sid,
|
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%;
|
||||||
max-width: 100%;
|
line-height: 1rem;
|
||||||
line-height: 1rem;
|
height: 1.1rem;
|
||||||
height: 1.1rem;
|
white-space: nowrap;
|
||||||
white-space: nowrap;
|
text-overflow: ellipsis;
|
||||||
text-overflow: ellipsis;
|
overflow: hidden;
|
||||||
overflow: hidden;
|
font-size: 16px;
|
||||||
font-size: 16px;
|
font-family: "Source Sans Pro", sans-serif;
|
||||||
font-family: "Source Sans Pro", sans-serif;
|
font-weight: bold;
|
||||||
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>
|
@ -1,34 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<b-modal :visible="show" size="lg" :hide-footer="true" static lazy @close="$emit('close')" @hide="$emit('close')"
|
<b-modal :visible="show" size="lg" :hide-footer="true" static lazy @close="$emit('close')" @hide="$emit('close')"
|
||||||
>
|
>
|
||||||
<template #modal-title>
|
<template #modal-title>
|
||||||
<h5 class="modal-title" :title="doc._source.name + ext(doc)">
|
<h5 class="modal-title" :title="doc._source.name + ext(doc)">
|
||||||
{{ doc._source.name + ext(doc) }}
|
{{ doc._source.name + ext(doc) }}
|
||||||
<router-link :to="`/file?byId=${doc._id}`">#</router-link>
|
<router-link :to="`/file?byId=${doc._id}`">#</router-link>
|
||||||
</h5>
|
</h5>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<img v-if="doc._props.hasThumbnail" :src="`t/${sid(doc)}`" alt="" class="fit card-img-top">
|
<img v-if="doc._props.hasThumbnail" :src="`t/${doc._source.index}/${doc._id}`" alt="" class="fit card-img-top">
|
||||||
|
|
||||||
<InfoTable :doc="doc"></InfoTable>
|
<InfoTable :doc="doc"></InfoTable>
|
||||||
|
|
||||||
<LazyContentDiv :sid="sid(doc)"></LazyContentDiv>
|
<LazyContentDiv :doc-id="doc._id"></LazyContentDiv>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {ext, sid} from "@/util";
|
import {ext} from "@/util";
|
||||||
import InfoTable from "@/components/InfoTable";
|
import InfoTable from "@/components/InfoTable";
|
||||||
import LazyContentDiv from "@/components/LazyContentDiv";
|
import LazyContentDiv from "@/components/LazyContentDiv";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "DocInfoModal",
|
name: "DocInfoModal",
|
||||||
components: {LazyContentDiv, InfoTable},
|
components: {LazyContentDiv, InfoTable},
|
||||||
props: ["doc", "show"],
|
props: ["doc", "show"],
|
||||||
methods: {
|
methods: {
|
||||||
ext: ext,
|
ext: ext,
|
||||||
sid: sid
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,49 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<b-list-group class="mt-3">
|
<b-list-group class="mt-3">
|
||||||
<DocListItem v-for="doc in docs" :key="sid(doc)" :doc="doc"></DocListItem>
|
<DocListItem v-for="doc in docs" :key="doc._id" :doc="doc"></DocListItem>
|
||||||
</b-list-group>
|
</b-list-group>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import {sid} from "@/util";
|
|
||||||
import DocListItem from "@/components/DocListItem.vue";
|
import DocListItem from "@/components/DocListItem.vue";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: "DocList",
|
name: "DocList",
|
||||||
components: {DocListItem},
|
components: {DocListItem},
|
||||||
props: ["docs", "append"],
|
props: ["docs", "append"],
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener("scroll", () => {
|
window.addEventListener("scroll", () => {
|
||||||
const threshold = 400;
|
const threshold = 400;
|
||||||
const app = document.getElementById("app");
|
const app = document.getElementById("app");
|
||||||
|
|
||||||
if ((window.innerHeight + window.scrollY) >= app.offsetHeight - threshold) {
|
if ((window.innerHeight + window.scrollY) >= app.offsetHeight - threshold) {
|
||||||
this.append();
|
this.append();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
sid: sid
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.theme-black .list-group-item {
|
.theme-black .list-group-item {
|
||||||
background: #212121;
|
background: #212121;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
|
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: .25rem 0.5rem;
|
padding: .25rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-black .list-group-item:first-child {
|
.theme-black .list-group-item:first-child {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
@ -1,64 +1,63 @@
|
|||||||
<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/${sid(doc)}` : `t/${sid(doc)}`"
|
: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/${sid(doc)}`" 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>
|
||||||
<MLIcon v-if="doc._source.embedding" clickable @click="onEmbeddingClick()"></MLIcon>
|
<DocFileTitle :doc="doc"></DocFileTitle>
|
||||||
<DocFileTitle :doc="doc"></DocFileTitle>
|
</div>
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- Featured line -->
|
|
||||||
<div style="display: flex">
|
|
||||||
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</b-list-group-item>
|
|
||||||
|
<!-- Featured line -->
|
||||||
|
<div style="display: flex">
|
||||||
|
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-list-group-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -68,142 +67,131 @@ 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";
|
|
||||||
import {sid} from "@/util";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "DocListItem",
|
name: "DocListItem",
|
||||||
components: {MLIcon, FileIcon, ContentDiv, DocInfoModal, DocFileTitle, TagContainer, FeaturedFieldsLine},
|
components: {FileIcon, ContentDiv, DocInfoModal, DocFileTitle, TagContainer, FeaturedFieldsLine},
|
||||||
props: ["doc"],
|
props: ["doc"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hover: false,
|
hover: false,
|
||||||
showInfo: false
|
showInfo: false
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
sid: sid,
|
|
||||||
async onThumbnailClick() {
|
|
||||||
this.$store.commit("setUiLightboxSlide", this.doc._seq);
|
|
||||||
await this.$store.dispatch("showLightbox");
|
|
||||||
},
|
|
||||||
onEmbeddingClick() {
|
|
||||||
Sist2Api.getEmbeddings(sid(this.doc), 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"]) {
|
|
||||||
return this.doc.highlight["path.nGram"] + "/"
|
|
||||||
}
|
|
||||||
return this.doc._source.path + "/"
|
|
||||||
},
|
|
||||||
onTnEnter() {
|
|
||||||
this.hover = true;
|
|
||||||
},
|
|
||||||
onTnLeave() {
|
|
||||||
this.hover = false;
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onThumbnailClick() {
|
||||||
|
this.$store.commit("setUiLightboxSlide", this.doc._seq);
|
||||||
|
await this.$store.dispatch("showLightbox");
|
||||||
|
},
|
||||||
|
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"]) {
|
||||||
|
return this.doc.highlight["path.nGram"] + "/"
|
||||||
|
}
|
||||||
|
return this.doc._source.path + "/"
|
||||||
|
},
|
||||||
|
onTnEnter() {
|
||||||
|
this.hover = true;
|
||||||
|
},
|
||||||
|
onTnLeave() {
|
||||||
|
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>
|
@ -1,155 +0,0 @@
|
|||||||
<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>
|
|
@ -1,189 +1,188 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="doc._props.hasThumbnail" class="img-wrapper" @mouseenter="onTnEnter()" @mouseleave="onTnLeave()"
|
<div v-if="doc._props.hasThumbnail" class="img-wrapper" @mouseenter="onTnEnter()" @mouseleave="onTnLeave()"
|
||||||
@touchstart="onTouchStart()">
|
@touchstart="onTouchStart()">
|
||||||
<div v-if="doc._props.isAudio" class="card-img-overlay" :class="{'small-badge': smallBadge}">
|
<div v-if="doc._props.isAudio" class="card-img-overlay" :class="{'small-badge': smallBadge}">
|
||||||
<span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span>
|
<span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="doc._props.isImage && doc._props.imageAspectRatio < 5"
|
|
||||||
class="card-img-overlay"
|
|
||||||
:class="{'small-badge': smallBadge}">
|
|
||||||
<span class="badge badge-resolution">{{ `${doc._source.width}x${doc._source.height}` }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="(doc._props.isVideo || doc._props.isGif) && doc._source.duration > 0"
|
|
||||||
class="card-img-overlay"
|
|
||||||
:class="{'small-badge': smallBadge}">
|
|
||||||
<span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="doc._props.isPlayableVideo" class="play">
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img ref="tn"
|
|
||||||
v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo"
|
|
||||||
:src="tnSrc"
|
|
||||||
alt=""
|
|
||||||
:style="{height: (doc._props.isGif && hover) ? `${tnHeight()}px` : undefined}"
|
|
||||||
class="pointer fit card-img-top" @click="onThumbnailClick()">
|
|
||||||
<img v-else :src="tnSrc" alt=""
|
|
||||||
class="fit card-img-top">
|
|
||||||
|
|
||||||
<ThumbnailProgressBar v-if="hover && doc._props.hasVidPreview"
|
|
||||||
:progress="(currentThumbnailNum + 1) / (doc._props.tnNum)"
|
|
||||||
></ThumbnailProgressBar>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="doc._props.isImage && doc._props.imageAspectRatio < 5"
|
||||||
|
class="card-img-overlay"
|
||||||
|
:class="{'small-badge': smallBadge}">
|
||||||
|
<span class="badge badge-resolution">{{ `${doc._source.width}x${doc._source.height}` }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="(doc._props.isVideo || doc._props.isGif) && doc._source.duration > 0"
|
||||||
|
class="card-img-overlay"
|
||||||
|
:class="{'small-badge': smallBadge}">
|
||||||
|
<span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="doc._props.isPlayableVideo" class="play">
|
||||||
|
<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"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img ref="tn"
|
||||||
|
v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo"
|
||||||
|
:src="tnSrc"
|
||||||
|
alt=""
|
||||||
|
:style="{height: (doc._props.isGif && hover) ? `${tnHeight()}px` : undefined}"
|
||||||
|
class="pointer fit card-img-top" @click="onThumbnailClick()">
|
||||||
|
<img v-else :src="tnSrc" alt=""
|
||||||
|
class="fit card-img-top">
|
||||||
|
|
||||||
|
<ThumbnailProgressBar v-if="hover && doc._props.hasVidPreview"
|
||||||
|
:progress="(currentThumbnailNum + 1) / (doc._props.tnNum)"
|
||||||
|
></ThumbnailProgressBar>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {humanTime, sid} from "@/util";
|
import {humanTime} from "@/util";
|
||||||
import ThumbnailProgressBar from "@/components/ThumbnailProgressBar";
|
import ThumbnailProgressBar from "@/components/ThumbnailProgressBar";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "FullThumbnail",
|
name: "FullThumbnail",
|
||||||
props: ["doc", "smallBadge"],
|
props: ["doc", "smallBadge"],
|
||||||
components: {ThumbnailProgressBar},
|
components: {ThumbnailProgressBar},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hover: false,
|
hover: false,
|
||||||
currentThumbnailNum: 0,
|
currentThumbnailNum: 0,
|
||||||
timeoutId: null
|
timeoutId: null
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.$store.subscribe((mutation) => {
|
|
||||||
if (mutation.type === "busTnTouchStart" && mutation.payload !== this.doc._id) {
|
|
||||||
this.onTnLeave();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
tnSrc() {
|
|
||||||
return this.getThumbnailSrc(this.currentThumbnailNum);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
sid: sid,
|
|
||||||
getThumbnailSrc(thumbnailNum) {
|
|
||||||
const doc = this.doc;
|
|
||||||
const props = doc._props;
|
|
||||||
if (props.isGif && this.hover) {
|
|
||||||
return `f/${sid(doc)}`;
|
|
||||||
}
|
|
||||||
return (this.currentThumbnailNum === 0)
|
|
||||||
? `t/${sid(doc)}`
|
|
||||||
: `t/${sid(doc)}/${String(thumbnailNum).padStart(4, "0")}`;
|
|
||||||
},
|
|
||||||
humanTime: humanTime,
|
|
||||||
onThumbnailClick() {
|
|
||||||
this.$emit("onThumbnailClick");
|
|
||||||
},
|
|
||||||
tnHeight() {
|
|
||||||
return this.$refs.tn.height;
|
|
||||||
},
|
|
||||||
tnWidth() {
|
|
||||||
return this.$refs.tn.width;
|
|
||||||
},
|
|
||||||
onTnEnter() {
|
|
||||||
this.hover = true;
|
|
||||||
const start = Date.now()
|
|
||||||
if (this.doc._props.hasVidPreview) {
|
|
||||||
let img = new Image();
|
|
||||||
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
|
|
||||||
img.onload = () => {
|
|
||||||
this.currentThumbnailNum += 1;
|
|
||||||
this.scheduleNextTnNum(Date.now() - start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTnLeave() {
|
|
||||||
this.currentThumbnailNum = 0;
|
|
||||||
this.hover = false;
|
|
||||||
if (this.timeoutId !== null) {
|
|
||||||
window.clearTimeout(this.timeoutId);
|
|
||||||
this.timeoutId = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scheduleNextTnNum(offset = 0) {
|
|
||||||
const INTERVAL = (this.$store.state.optVidPreviewInterval ?? 700) - offset;
|
|
||||||
this.timeoutId = window.setTimeout(() => {
|
|
||||||
const start = Date.now();
|
|
||||||
if (!this.hover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.currentThumbnailNum === this.doc._props.tnNum - 1) {
|
|
||||||
this.currentThumbnailNum = 0;
|
|
||||||
this.scheduleNextTnNum();
|
|
||||||
} else {
|
|
||||||
let img = new Image();
|
|
||||||
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
|
|
||||||
img.onload = () => {
|
|
||||||
this.currentThumbnailNum += 1;
|
|
||||||
this.scheduleNextTnNum(Date.now() - start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, INTERVAL);
|
|
||||||
},
|
|
||||||
onTouchStart() {
|
|
||||||
this.$store.commit("busTnTouchStart", this.doc._id);
|
|
||||||
if (!this.hover) {
|
|
||||||
this.onTnEnter()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$store.subscribe((mutation) => {
|
||||||
|
if (mutation.type === "busTnTouchStart" && mutation.payload !== this.doc._id) {
|
||||||
|
this.onTnLeave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tnSrc() {
|
||||||
|
return this.getThumbnailSrc(this.currentThumbnailNum);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getThumbnailSrc(thumbnailNum) {
|
||||||
|
const doc = this.doc;
|
||||||
|
const props = doc._props;
|
||||||
|
if (props.isGif && this.hover) {
|
||||||
|
return `f/${doc._source.index}/${doc._id}`;
|
||||||
|
}
|
||||||
|
return (this.currentThumbnailNum === 0)
|
||||||
|
? `t/${doc._source.index}/${doc._id}`
|
||||||
|
: `t/${doc._source.index}/${doc._id}/${String(thumbnailNum).padStart(4, "0")}`;
|
||||||
|
},
|
||||||
|
humanTime: humanTime,
|
||||||
|
onThumbnailClick() {
|
||||||
|
this.$emit("onThumbnailClick");
|
||||||
|
},
|
||||||
|
tnHeight() {
|
||||||
|
return this.$refs.tn.height;
|
||||||
|
},
|
||||||
|
tnWidth() {
|
||||||
|
return this.$refs.tn.width;
|
||||||
|
},
|
||||||
|
onTnEnter() {
|
||||||
|
this.hover = true;
|
||||||
|
const start = Date.now()
|
||||||
|
if (this.doc._props.hasVidPreview) {
|
||||||
|
let img = new Image();
|
||||||
|
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
|
||||||
|
img.onload = () => {
|
||||||
|
this.currentThumbnailNum += 1;
|
||||||
|
this.scheduleNextTnNum(Date.now() - start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTnLeave() {
|
||||||
|
this.currentThumbnailNum = 0;
|
||||||
|
this.hover = false;
|
||||||
|
if (this.timeoutId !== null) {
|
||||||
|
window.clearTimeout(this.timeoutId);
|
||||||
|
this.timeoutId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scheduleNextTnNum(offset = 0) {
|
||||||
|
const INTERVAL = (this.$store.state.optVidPreviewInterval ?? 700) - offset;
|
||||||
|
this.timeoutId = window.setTimeout(() => {
|
||||||
|
const start = Date.now();
|
||||||
|
if (!this.hover) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.currentThumbnailNum === this.doc._props.tnNum - 1) {
|
||||||
|
this.currentThumbnailNum = 0;
|
||||||
|
this.scheduleNextTnNum();
|
||||||
|
} else {
|
||||||
|
let img = new Image();
|
||||||
|
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
|
||||||
|
img.onload = () => {
|
||||||
|
this.currentThumbnailNum += 1;
|
||||||
|
this.scheduleNextTnNum(Date.now() - start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, INTERVAL);
|
||||||
|
},
|
||||||
|
onTouchStart() {
|
||||||
|
this.$store.commit("busTnTouchStart", this.doc._id);
|
||||||
|
if (!this.hover) {
|
||||||
|
this.onTnEnter()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.img-wrapper {
|
.img-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.img-wrapper:hover svg {
|
.img-wrapper:hover svg {
|
||||||
fill: rgba(0, 0, 0, 1);
|
fill: rgba(0, 0, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-img-top {
|
.card-img-top {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.play {
|
.play {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-resolution {
|
.badge-resolution {
|
||||||
color: #c6c6c6;
|
color: #c6c6c6;
|
||||||
background-color: #272727CC;
|
background-color: #272727CC;
|
||||||
padding: 2px 3px;
|
padding: 2px 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-img-overlay {
|
.card-img-overlay {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
top: unset;
|
top: unset;
|
||||||
left: unset;
|
left: unset;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-badge {
|
.small-badge {
|
||||||
padding: 1px 3px;
|
padding: 1px 3px;
|
||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
@ -1,201 +1,191 @@
|
|||||||
<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}"
|
|
||||||
:key="idx.id"
|
|
||||||
>
|
|
||||||
<div class="d-flex">
|
|
||||||
<b-checkbox style="pointer-events: none" :checked="isSelected(idx)"></b-checkbox>
|
|
||||||
{{ idx.name }}
|
|
||||||
<div v-if="hasEmbeddings(idx)" 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>
|
</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>
|
||||||
|
<b-badge class="version-badge">v{{ idx.version }}</b-badge>
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import SmallBadge from "./SmallBadge.vue"
|
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: {
|
||||||
MLIcon,
|
SmallBadge
|
||||||
SmallBadge
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
lastClickIndex: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
"indices", "selectedIndices"
|
||||||
|
]),
|
||||||
|
selectedIndicesIds() {
|
||||||
|
return this.selectedIndices.map(idx => idx.id)
|
||||||
},
|
},
|
||||||
data() {
|
isMobile() {
|
||||||
return {
|
return window.innerWidth <= 650;
|
||||||
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]));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
selectAll() {
|
||||||
...mapGetters([
|
this.setSelectedIndices(this.indices);
|
||||||
"indices", "selectedIndices"
|
|
||||||
]),
|
|
||||||
selectedIndicesIds() {
|
|
||||||
return this.selectedIndices.map(idx => idx.id)
|
|
||||||
},
|
|
||||||
isMobile() {
|
|
||||||
return window.innerWidth <= 650;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
selectNone() {
|
||||||
...mapActions({
|
this.setSelectedIndices([]);
|
||||||
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() {
|
|
||||||
this.setSelectedIndices(this.indices);
|
|
||||||
},
|
|
||||||
selectNone() {
|
|
||||||
this.setSelectedIndices([]);
|
|
||||||
},
|
|
||||||
onSelect(value) {
|
|
||||||
this.setSelectedIndices(this.indices.filter(idx => value.includes(idx.id)));
|
|
||||||
},
|
|
||||||
formatIdxDate(timestamp) {
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
hasEmbeddings(index) {
|
|
||||||
return index.models.length > 0;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</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>
|
@ -59,7 +59,7 @@ export default {
|
|||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
"title", "duration", "audioc", "videoc",
|
"title", "duration", "audioc", "videoc",
|
||||||
"bitrate", "artist", "album", "album_artist", "genre", "font_name", "author", "media_comment",
|
"bitrate", "artist", "album", "album_artist", "genre", "font_name", "author",
|
||||||
"modified_by", "pages", "tag",
|
"modified_by", "pages", "tag",
|
||||||
"exif_make", "exif_software", "exif_exposure_time", "exif_fnumber", "exif_focal_length",
|
"exif_make", "exif_software", "exif_exposure_time", "exif_fnumber", "exif_focal_length",
|
||||||
"exif_user_comment", "exif_iso_speed_ratings", "exif_model", "exif_datetime",
|
"exif_user_comment", "exif_iso_speed_ratings", "exif_model", "exif_datetime",
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
<b-button :disabled="mlPredictionsLoading || mlLoading" @click="mlAnalyze" variant="primary"
|
<b-button :disabled="mlPredictionsLoading || mlLoading" @click="mlAnalyze" variant="primary"
|
||||||
>{{ $t("ml.analyzeText") }}
|
>{{ $t("ml.analyzeText") }}
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-select :disabled="mlPredictionsLoading || mlLoading" class="ml-2" v-model="nerModel">
|
<b-select :disabled="mlPredictionsLoading || mlLoading" class="ml-2" v-model="mlModel">
|
||||||
<b-select-option :value="opt.value" v-for="opt of ModelsRepo.getOptions()" :key="opt.value">{{ opt.text }}
|
<b-select-option :value="opt.value" v-for="opt of ModelsRepo.getOptions()">{{ opt.text }}
|
||||||
</b-select-option>
|
</b-select-option>
|
||||||
</b-select>
|
</b-select>
|
||||||
</b-form>
|
</b-form>
|
||||||
@ -46,7 +46,7 @@ import {mapGetters, mapMutations} from "vuex";
|
|||||||
export default {
|
export default {
|
||||||
name: "LazyContentDiv",
|
name: "LazyContentDiv",
|
||||||
components: {AnalyzedContentSpansContainer, Preloader},
|
components: {AnalyzedContentSpansContainer, Preloader},
|
||||||
props: ["sid"],
|
props: ["docId"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ModelsRepo,
|
ModelsRepo,
|
||||||
@ -57,20 +57,20 @@ export default {
|
|||||||
modelPredictionProgress: 0,
|
modelPredictionProgress: 0,
|
||||||
mlPredictionsLoading: false,
|
mlPredictionsLoading: false,
|
||||||
mlLoading: false,
|
mlLoading: false,
|
||||||
nerModel: null,
|
mlModel: null,
|
||||||
analyzedContentSpans: []
|
analyzedContentSpans: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
||||||
if (this.$store.getters.optMlDefaultModel) {
|
if (this.$store.getters.optMlDefaultModel) {
|
||||||
this.nerModel = this.$store.getters.optMlDefaultModel
|
this.mlModel = this.$store.getters.optMlDefaultModel
|
||||||
} else {
|
} else {
|
||||||
this.nerModel = ModelsRepo.getDefaultModel();
|
this.mlModel = ModelsRepo.getDefaultModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
Sist2Api
|
Sist2Api
|
||||||
.getDocument(this.sid, this.$store.state.optHighlight, this.$store.state.fuzzy)
|
.getDocument(this.docId, this.$store.state.optHighlight, this.$store.state.fuzzy)
|
||||||
.then(doc => {
|
.then(doc => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters(["optAutoAnalyze"]),
|
...mapGetters(["optAutoAnalyze"]),
|
||||||
modelSize() {
|
modelSize() {
|
||||||
const modelData = ModelsRepo.data[this.nerModel];
|
const modelData = ModelsRepo.data[this.mlModel];
|
||||||
if (!modelData) {
|
if (!modelData) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -110,10 +110,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getMlModel() {
|
async getMlModel() {
|
||||||
if (this.$store.getters.nerModel.name !== this.nerModel) {
|
if (this.$store.getters.mlModel.name !== this.mlModel) {
|
||||||
this.mlLoading = true;
|
this.mlLoading = true;
|
||||||
this.modelLoadingProgress = 0;
|
this.modelLoadingProgress = 0;
|
||||||
const modelInfo = ModelsRepo.data[this.nerModel];
|
const modelInfo = ModelsRepo.data[this.mlModel];
|
||||||
|
|
||||||
const model = new BertNerModel(
|
const model = new BertNerModel(
|
||||||
modelInfo.vocabUrl,
|
modelInfo.vocabUrl,
|
||||||
@ -122,25 +122,25 @@ export default {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await model.init(progress => this.modelLoadingProgress = progress);
|
await model.init(progress => this.modelLoadingProgress = progress);
|
||||||
this.$store.commit("setNerModel", {model, name: this.nerModel});
|
this.$store.commit("setMlModel", {model, name: this.mlModel});
|
||||||
|
|
||||||
this.mlLoading = false;
|
this.mlLoading = false;
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.$store.getters.nerModel.model;
|
return this.$store.getters.mlModel.model;
|
||||||
},
|
},
|
||||||
async mlAnalyze() {
|
async mlAnalyze() {
|
||||||
if (!this.content) {
|
if (!this.content) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelInfo = ModelsRepo.data[this.nerModel];
|
const modelInfo = ModelsRepo.data[this.mlModel];
|
||||||
if (modelInfo === undefined) {
|
if (modelInfo === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit("setOptMlDefaultModel", this.nerModel);
|
this.$store.commit("setOptMlDefaultModel", this.mlModel);
|
||||||
await this.$store.dispatch("updateConfiguration");
|
await this.$store.dispatch("updateConfiguration");
|
||||||
|
|
||||||
const model = await this.getMlModel();
|
const model = await this.getMlModel();
|
||||||
|
@ -77,7 +77,6 @@ export default {
|
|||||||
return listener(e);
|
return listener(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
keyDownListener(e) {
|
keyDownListener(e) {
|
||||||
|
@ -9,7 +9,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/sist2app/sist2/blob/master/docs/USAGE.md#elasticsearch"
|
href="https://github.com/simon987/sist2/blob/master/docs/USAGE.md#elasticsearch"
|
||||||
target="_blank">legacyES</a></span><span v-if="$store.state.uiSqliteMode">-SQLite</span>
|
target="_blank">legacyES</a></span><span v-if="$store.state.uiSqliteMode">-SQLite</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -43,7 +43,8 @@
|
|||||||
</b-card>
|
</b-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
|
import Sist2Api, {EsResult} from "@/Sist2Api";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import {humanFileSize} from "@/util";
|
import {humanFileSize} from "@/util";
|
||||||
import DisplayModeToggle from "@/components/DisplayModeToggle.vue";
|
import DisplayModeToggle from "@/components/DisplayModeToggle.vue";
|
||||||
@ -51,7 +52,6 @@ import SortSelect from "@/components/SortSelect.vue";
|
|||||||
import Preloader from "@/components/Preloader.vue";
|
import Preloader from "@/components/Preloader.vue";
|
||||||
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
||||||
import ClipboardIcon from "@/components/icons/ClipboardIcon.vue";
|
import ClipboardIcon from "@/components/icons/ClipboardIcon.vue";
|
||||||
import Sist2Api from "@/Sist2Api";
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: "ResultsCard",
|
name: "ResultsCard",
|
||||||
@ -64,7 +64,7 @@ export default Vue.extend({
|
|||||||
return this.$store.state.lastQueryResults != null;
|
return this.$store.state.lastQueryResults != null;
|
||||||
},
|
},
|
||||||
hitCount() {
|
hitCount() {
|
||||||
return (this.$store.state.firstQueryResults).aggregations.total_count.value;
|
return (this.$store.state.firstQueryResults as EsResult).aggregations.total_count.value;
|
||||||
},
|
},
|
||||||
tableItems() {
|
tableItems() {
|
||||||
const items = [];
|
const items = [];
|
||||||
@ -79,10 +79,10 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
took() {
|
took() {
|
||||||
return (this.$store.state.lastQueryResults).took + "ms";
|
return (this.$store.state.lastQueryResults as EsResult).took + "ms";
|
||||||
},
|
},
|
||||||
totalSize() {
|
totalSize() {
|
||||||
return humanFileSize((this.$store.state.firstQueryResults).aggregations.total_size.value);
|
return humanFileSize((this.$store.state.firstQueryResults as EsResult).aggregations.total_size.value);
|
||||||
},
|
},
|
||||||
onToggle() {
|
onToggle() {
|
||||||
const show = !document.getElementById("collapse-1").classList.contains("show");
|
const show = !document.getElementById("collapse-1").classList.contains("show");
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import noUiSlider from 'nouislider';
|
import noUiSlider from 'nouislider';
|
||||||
import 'nouislider/dist/nouislider.css';
|
import 'nouislider/dist/nouislider.css';
|
||||||
import {humanFileSize, mergeTooltips} from "@/util";
|
import {humanFileSize} from "@/util";
|
||||||
|
import {mergeTooltips} from "@/util-js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SizeSlider",
|
name: "SizeSlider",
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<b-badge variant="secondary" :pill="pill">{{ text }}</b-badge>
|
<b-badge variant="secondary" :pill="pill">{{ text }}</b-badge>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<b-dropdown variant="primary" :disabled="$store.getters.embedding !== null">
|
<b-dropdown variant="primary">
|
||||||
<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")
|
||||||
}}
|
}}
|
||||||
|
@ -210,8 +210,4 @@ export default {
|
|||||||
.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>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="thumbnail_count-progress-bar" :style="{width: `${percentProgress}%`}"></div>
|
<div class="thumbnail-progress-bar" :style="{width: `${percentProgress}%`}"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -16,7 +16,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.thumbnail_count-progress-bar {
|
.thumbnail-progress-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@ -27,11 +27,11 @@ export default {
|
|||||||
z-index: 9;
|
z-index: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-black .thumbnail_count-progress-bar {
|
.theme-black .thumbnail-progress-bar {
|
||||||
background: rgba(0, 188, 212, 0.95);
|
background: rgba(0, 188, 212, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-document .thumbnail_count-progress-bar {
|
.sub-document .thumbnail-progress-bar {
|
||||||
max-width: calc(100% - 8px);
|
max-width: calc(100% - 8px);
|
||||||
left: 4px;
|
left: 4px;
|
||||||
}
|
}
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
<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>
|
|
@ -18,7 +18,6 @@ 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",
|
||||||
@ -138,7 +137,7 @@ export default {
|
|||||||
},
|
},
|
||||||
debug: "Debug information",
|
debug: "Debug information",
|
||||||
debugDescription: "Information useful for debugging. If you encounter bugs or have suggestions for" +
|
debugDescription: "Information useful for debugging. If you encounter bugs or have suggestions for" +
|
||||||
" new features, please submit a new issue <a href='https://github.com/sist2app/sist2/issues/new/choose'>here</a>.",
|
" new features, please submit a new issue <a href='https://github.com/simon987/sist2/issues/new/choose'>here</a>.",
|
||||||
tagline: "Tagline",
|
tagline: "Tagline",
|
||||||
toast: {
|
toast: {
|
||||||
esConnErrTitle: "Elasticsearch connection error",
|
esConnErrTitle: "Elasticsearch connection error",
|
||||||
@ -318,7 +317,7 @@ export default {
|
|||||||
},
|
},
|
||||||
debug: "Debug Informationen",
|
debug: "Debug Informationen",
|
||||||
debugDescription: "Informationen für das Debugging. Wenn du Bugs gefunden oder Anregungen für " +
|
debugDescription: "Informationen für das Debugging. Wenn du Bugs gefunden oder Anregungen für " +
|
||||||
"neue Features hast, poste sie bitte <a href='https://github.com/sist2app/sist2/issues/new/choose'>hier</a>.",
|
"neue Features hast, poste sie bitte <a href='https://github.com/simon987/sist2/issues/new/choose'>hier</a>.",
|
||||||
tagline: "Tagline",
|
tagline: "Tagline",
|
||||||
toast: {
|
toast: {
|
||||||
esConnErrTitle: "Elasticsearch Verbindungsfehler",
|
esConnErrTitle: "Elasticsearch Verbindungsfehler",
|
||||||
@ -494,7 +493,7 @@ export default {
|
|||||||
debug: "Information de débogage",
|
debug: "Information de débogage",
|
||||||
debugDescription: "Informations utiles pour le débogage\n" +
|
debugDescription: "Informations utiles pour le débogage\n" +
|
||||||
"Si vous rencontrez des bogues ou si vous avez des suggestions pour de nouvelles fonctionnalités," +
|
"Si vous rencontrez des bogues ou si vous avez des suggestions pour de nouvelles fonctionnalités," +
|
||||||
" veuillez soumettre un nouvel Issue <a href='https://github.com/sist2app/sist2/issues/new/choose'>ici</a>.",
|
" veuillez soumettre un nouvel Issue <a href='https://github.com/simon987/sist2/issues/new/choose'>ici</a>.",
|
||||||
tagline: "Tagline",
|
tagline: "Tagline",
|
||||||
toast: {
|
toast: {
|
||||||
esConnErrTitle: "Erreur de connexion Elasticsearch",
|
esConnErrTitle: "Erreur de connexion Elasticsearch",
|
||||||
@ -668,7 +667,7 @@ export default {
|
|||||||
},
|
},
|
||||||
debug: "调试信息",
|
debug: "调试信息",
|
||||||
debugDescription: "对调试除错有用的信息。 若您遇到bug或者想建议新功能,请提交新Issue到" +
|
debugDescription: "对调试除错有用的信息。 若您遇到bug或者想建议新功能,请提交新Issue到" +
|
||||||
"<a href='https://github.com/sist2app/sist2/issues/new/choose'>这里</a>.",
|
"<a href='https://github.com/simon987/sist2/issues/new/choose'>这里</a>.",
|
||||||
tagline: "标签栏",
|
tagline: "标签栏",
|
||||||
toast: {
|
toast: {
|
||||||
esConnErrTitle: "Elasticsearch连接错误",
|
esConnErrTitle: "Elasticsearch连接错误",
|
||||||
@ -846,7 +845,7 @@ export default {
|
|||||||
},
|
},
|
||||||
debug: "Informacje dla programistów",
|
debug: "Informacje dla programistów",
|
||||||
debugDescription: "Informacje przydatne do znajdowania błędów w oprogramowaniu. Jeśli napotkasz błąd lub masz" +
|
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/sist2app/sist2/issues/new/choose'>tutaj</a>.",
|
" propozycje zmian, zgłoś to proszę <a href='https://github.com/simon987/sist2/issues/new/choose'>tutaj</a>.",
|
||||||
tagline: "Slogan",
|
tagline: "Slogan",
|
||||||
toast: {
|
toast: {
|
||||||
esConnErrTitle: "Problem z połączeniem z Elasticsearch",
|
esConnErrTitle: "Problem z połączeniem z Elasticsearch",
|
@ -1,3 +1,4 @@
|
|||||||
|
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'
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,6 @@
|
|||||||
import BertTokenizer from "@/ml/BertTokenizer";
|
import BertTokenizer from "@/ml/BertTokenizer";
|
||||||
|
import * as tf from "@tensorflow/tfjs";
|
||||||
import axios from "axios";
|
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 {
|
export default class BertNerModel {
|
||||||
vocabUrl;
|
vocabUrl;
|
||||||
@ -31,10 +29,7 @@ export default class BertNerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadModel(onProgress) {
|
async loadModel(onProgress) {
|
||||||
ort.env.wasm.wasmPaths = ORT_WASM_PATHS;
|
this._model = await tf.loadGraphModel(this.modelUrl, {onProgress});
|
||||||
const buf = await downloadToBuffer(this.modelUrl, onProgress);
|
|
||||||
|
|
||||||
this._model = await ort.InferenceSession.create(buf.buffer, {executionProviders: ["wasm"]});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alignLabels(labels, wordIds, words) {
|
alignLabels(labels, wordIds, words) {
|
||||||
@ -62,28 +57,21 @@ export default class BertNerModel {
|
|||||||
|
|
||||||
async predict(text, callback) {
|
async predict(text, callback) {
|
||||||
this._previousWordId = null;
|
this._previousWordId = null;
|
||||||
const encoded = this._tokenizer.encodeText(text, this.inputSize);
|
const encoded = this._tokenizer.encodeText(text, this.inputSize)
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
for (let chunk of encoded.inputChunks) {
|
for (let chunk of encoded.inputChunks) {
|
||||||
|
const rawResult = tf.tidy(() => this._model.execute({
|
||||||
|
input_ids: tf.tensor2d(chunk.inputIds, [1, this.inputSize], "int32"),
|
||||||
|
token_type_ids: tf.tensor2d(chunk.segmentIds, [1, this.inputSize], "int32"),
|
||||||
|
attention_mask: tf.tensor2d(chunk.inputMask, [1, this.inputSize], "int32"),
|
||||||
|
}));
|
||||||
|
|
||||||
const results = await this._model.run({
|
const labelIds = await tf.argMax(rawResult, -1);
|
||||||
input_ids: new ort.Tensor("int32", chunk.inputIds, [1, this.inputSize]),
|
const labelIdsArray = await labelIds.array();
|
||||||
token_type_ids: new ort.Tensor("int32", chunk.segmentIds, [1, this.inputSize]),
|
const labels = labelIdsArray[0].map(id => this.id2label[id]);
|
||||||
attention_mask: new ort.Tensor("int32", chunk.inputMask, [1, this.inputSize]),
|
rawResult.dispose()
|
||||||
});
|
|
||||||
|
|
||||||
const labelIds = _chunk(results["output"].data, this.id2label.length).map(argMax);
|
callback(this.alignLabels(labels, chunk.wordIds, encoded.words))
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import {zip, chunk} from "underscore";
|
import {zip, chunk} from "underscore";
|
||||||
import {toInt64} from "@/ml/mlUtils";
|
|
||||||
|
|
||||||
const UNK_INDEX = 100;
|
const UNK_INDEX = 100;
|
||||||
const CLS_INDEX = 101;
|
const CLS_INDEX = 101;
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
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;
|
|
||||||
if (window.crossOriginIsolated) {
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
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();
|
|
@ -1,46 +0,0 @@
|
|||||||
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",
|
|
||||||
}
|
|
@ -21,7 +21,7 @@ const authGuard = (to, from, next) => {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [
|
const routes: Array<RouteConfig> = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "SearchPage",
|
name: "SearchPage",
|
@ -1,18 +1,20 @@
|
|||||||
import Vue from "vue"
|
import Vue from "vue"
|
||||||
import Vuex from "vuex"
|
import Vuex from "vuex"
|
||||||
|
import VueRouter, {Route} from "vue-router";
|
||||||
|
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 = 3;
|
const CONF_VERSION = 3;
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex)
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
seed: 0,
|
seed: 0,
|
||||||
indices: [],
|
indices: [] as Index[],
|
||||||
tags: [],
|
tags: [] as EsTag[],
|
||||||
sist2Info: null,
|
sist2Info: null as any,
|
||||||
|
|
||||||
sizeMin: undefined,
|
sizeMin: undefined,
|
||||||
sizeMax: undefined,
|
sizeMax: undefined,
|
||||||
@ -21,9 +23,6 @@ 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",
|
||||||
|
|
||||||
@ -58,16 +57,16 @@ export default new Vuex.Store({
|
|||||||
optVidPreviewInterval: 700,
|
optVidPreviewInterval: 700,
|
||||||
optSimpleLightbox: true,
|
optSimpleLightbox: true,
|
||||||
optShowTagPickerFilter: true,
|
optShowTagPickerFilter: true,
|
||||||
optMlRepositories: "https://raw.githubusercontent.com/sist2app/sist2-ner-models/main/repo.json",
|
optMlRepositories: "https://raw.githubusercontent.com/simon987/sist2-ner-models/main/repo.json",
|
||||||
optAutoAnalyze: false,
|
optAutoAnalyze: false,
|
||||||
optMlDefaultModel: null,
|
optMlDefaultModel: null,
|
||||||
|
|
||||||
_onLoadSelectedIndices: [],
|
_onLoadSelectedIndices: [] as string[],
|
||||||
_onLoadSelectedMimeTypes: [],
|
_onLoadSelectedMimeTypes: [] as string[],
|
||||||
_onLoadSelectedTags: [],
|
_onLoadSelectedTags: [] as string[],
|
||||||
selectedIndices: [],
|
selectedIndices: [] as Index[],
|
||||||
selectedMimeTypes: [],
|
selectedMimeTypes: [] as string[],
|
||||||
selectedTags: [],
|
selectedTags: [] as string[],
|
||||||
|
|
||||||
lastQueryResults: null,
|
lastQueryResults: null,
|
||||||
firstQueryResults: null,
|
firstQueryResults: null,
|
||||||
@ -78,10 +77,10 @@ export default new Vuex.Store({
|
|||||||
uiSqliteMode: false,
|
uiSqliteMode: false,
|
||||||
uiLightboxIsOpen: false,
|
uiLightboxIsOpen: false,
|
||||||
uiShowLightbox: false,
|
uiShowLightbox: false,
|
||||||
uiLightboxSources: [],
|
uiLightboxSources: [] as string[],
|
||||||
uiLightboxThumbs: [],
|
uiLightboxThumbs: [] as string[],
|
||||||
uiLightboxCaptions: [],
|
uiLightboxCaptions: [] as any[],
|
||||||
uiLightboxTypes: [],
|
uiLightboxTypes: [] as string[],
|
||||||
uiLightboxKey: 0,
|
uiLightboxKey: 0,
|
||||||
uiLightboxSlide: 0,
|
uiLightboxSlide: 0,
|
||||||
uiReachedScrollEnd: false,
|
uiReachedScrollEnd: false,
|
||||||
@ -89,14 +88,13 @@ export default new Vuex.Store({
|
|||||||
uiDetailsMimeAgg: null,
|
uiDetailsMimeAgg: null,
|
||||||
uiShowDetails: false,
|
uiShowDetails: false,
|
||||||
|
|
||||||
uiMimeMap: [],
|
uiMimeMap: [] as any[],
|
||||||
|
|
||||||
auth0Token: null,
|
auth0Token: null,
|
||||||
nerModel: {
|
mlModel: {
|
||||||
model: null,
|
model: null,
|
||||||
name: null
|
name: null
|
||||||
},
|
},
|
||||||
embeddingsModel: null
|
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setUiShowDetails: (state, val) => state.uiShowDetails = val,
|
setUiShowDetails: (state, val) => state.uiShowDetails = val,
|
||||||
@ -120,7 +118,7 @@ export default new Vuex.Store({
|
|||||||
if (state._onLoadSelectedIndices.length > 0) {
|
if (state._onLoadSelectedIndices.length > 0) {
|
||||||
|
|
||||||
state.selectedIndices = val.filter(
|
state.selectedIndices = val.filter(
|
||||||
(idx) => state._onLoadSelectedIndices.some(id => id === idx.id.toString(16))
|
(idx: Index) => state._onLoadSelectedIndices.some(prefix => idx.id.startsWith(prefix))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
state.selectedIndices = val;
|
state.selectedIndices = val;
|
||||||
@ -131,9 +129,6 @@ 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,
|
setFirstQueryResult: (state, val) => state.firstQueryResults = val,
|
||||||
@ -143,18 +138,18 @@ export default new Vuex.Store({
|
|||||||
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,
|
||||||
setUiLightboxIsOpen: (state, val) => state.uiLightboxIsOpen = val,
|
setUiLightboxIsOpen: (state, val: boolean) => state.uiLightboxIsOpen = val,
|
||||||
_setUiShowLightbox: (state, val) => state.uiShowLightbox = val,
|
_setUiShowLightbox: (state, val: boolean) => state.uiShowLightbox = val,
|
||||||
setUiLightboxKey: (state, val) => state.uiLightboxKey = val,
|
setUiLightboxKey: (state, val: number) => state.uiLightboxKey = val,
|
||||||
_setKeySequence: (state, val) => state.keySequence = val,
|
_setKeySequence: (state, val: number) => state.keySequence = val,
|
||||||
_setQuerySequence: (state, val) => state.querySequence = val,
|
_setQuerySequence: (state, val: number) => state.querySequence = val,
|
||||||
addLightboxSource: (state, {source, thumbnail, caption, type}) => {
|
addLightboxSource: (state, {source, thumbnail, caption, type}) => {
|
||||||
state.uiLightboxSources.push(source);
|
state.uiLightboxSources.push(source);
|
||||||
state.uiLightboxThumbs.push(thumbnail);
|
state.uiLightboxThumbs.push(thumbnail);
|
||||||
state.uiLightboxCaptions.push(caption);
|
state.uiLightboxCaptions.push(caption);
|
||||||
state.uiLightboxTypes.push(type);
|
state.uiLightboxTypes.push(type);
|
||||||
},
|
},
|
||||||
setUiLightboxSlide: (state, val) => state.uiLightboxSlide = val,
|
setUiLightboxSlide: (state, val: number) => state.uiLightboxSlide = val,
|
||||||
|
|
||||||
setUiLightboxSources: (state, val) => state.uiLightboxSources = val,
|
setUiLightboxSources: (state, val) => state.uiLightboxSources = val,
|
||||||
setUiLightboxThumbs: (state, val) => state.uiLightboxThumbs = val,
|
setUiLightboxThumbs: (state, val) => state.uiLightboxThumbs = val,
|
||||||
@ -217,8 +212,7 @@ 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,
|
setMlModel: (state, val) => state.mlModel = val,
|
||||||
setEmbeddingsModel: (state, val) => state.embeddingsModel = val,
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setSist2Info: (store, val) => {
|
setSist2Info: (store, val) => {
|
||||||
@ -228,7 +222,7 @@ export default new Vuex.Store({
|
|||||||
store.commit("setOptLang", val.lang);
|
store.commit("setOptLang", val.lang);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadFromArgs({commit}, route) {
|
loadFromArgs({commit}, route: Route) {
|
||||||
|
|
||||||
if (route.query.q) {
|
if (route.query.q) {
|
||||||
commit("setSearchText", route.query.q);
|
commit("setSearchText", route.query.q);
|
||||||
@ -263,11 +257,11 @@ export default new Vuex.Store({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.m) {
|
if (route.query.m) {
|
||||||
commit("_setOnLoadSelectedMimeTypes", deserializeMimes(route.query.m));
|
commit("_setOnLoadSelectedMimeTypes", deserializeMimes(route.query.m as string));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.t) {
|
if (route.query.t) {
|
||||||
commit("_setOnLoadSelectedTags", (route.query.t).split(","));
|
commit("_setOnLoadSelectedTags", (route.query.t as string).split(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.sort) {
|
if (route.query.sort) {
|
||||||
@ -278,7 +272,7 @@ export default new Vuex.Store({
|
|||||||
commit("setSeed", Number(route.query.seed));
|
commit("setSeed", Number(route.query.seed));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async updateArgs({state}, router) {
|
async updateArgs({state}, router: VueRouter) {
|
||||||
|
|
||||||
if (router.currentRoute.path !== "/") {
|
if (router.currentRoute.path !== "/") {
|
||||||
return;
|
return;
|
||||||
@ -288,14 +282,14 @@ export default new Vuex.Store({
|
|||||||
query: {
|
query: {
|
||||||
q: state.searchText.trim() ? state.searchText.trim().replace(/\s+/g, " ") : undefined,
|
q: state.searchText.trim() ? state.searchText.trim().replace(/\s+/g, " ") : undefined,
|
||||||
fuzzy: state.fuzzy ? null : undefined,
|
fuzzy: state.fuzzy ? null : undefined,
|
||||||
i: state.selectedIndices ? state.selectedIndices.map((idx) => idx.id.toString(16)) : undefined,
|
i: state.selectedIndices ? state.selectedIndices.map((idx: Index) => idx.idPrefix) : undefined,
|
||||||
dMin: state.dateMin,
|
dMin: state.dateMin,
|
||||||
dMax: state.dateMax,
|
dMax: state.dateMax,
|
||||||
sMin: state.sizeMin,
|
sMin: state.sizeMin,
|
||||||
sMax: state.sizeMax,
|
sMax: state.sizeMax,
|
||||||
path: state.pathText ? state.pathText : undefined,
|
path: state.pathText ? state.pathText : undefined,
|
||||||
m: serializeMimes(state.selectedMimeTypes),
|
m: serializeMimes(state.selectedMimeTypes),
|
||||||
t: state.selectedTags.length === 0 ? undefined : state.selectedTags.join(","),
|
t: state.selectedTags.length == 0 ? undefined : state.selectedTags.join(","),
|
||||||
sort: state.sortMode === "score" ? undefined : state.sortMode,
|
sort: state.sortMode === "score" ? undefined : state.sortMode,
|
||||||
seed: state.sortMode === "random" ? state.seed.toString() : undefined
|
seed: state.sortMode === "random" ? state.seed.toString() : undefined
|
||||||
}
|
}
|
||||||
@ -304,11 +298,11 @@ export default new Vuex.Store({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateConfiguration({state}) {
|
updateConfiguration({state}) {
|
||||||
const conf = {};
|
const conf = {} as any;
|
||||||
|
|
||||||
Object.keys(state).forEach((key) => {
|
Object.keys(state).forEach((key) => {
|
||||||
if (key.startsWith("opt")) {
|
if (key.startsWith("opt")) {
|
||||||
conf[key] = (state)[key];
|
conf[key] = (state as any)[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -321,14 +315,14 @@ export default new Vuex.Store({
|
|||||||
if (confString) {
|
if (confString) {
|
||||||
const conf = JSON.parse(confString);
|
const conf = JSON.parse(confString);
|
||||||
|
|
||||||
if (!("version" in conf) || conf["version"] !== CONF_VERSION) {
|
if (!("version" in conf) || conf["version"] != CONF_VERSION) {
|
||||||
localStorage.removeItem("sist2_configuration");
|
localStorage.removeItem("sist2_configuration");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(state).forEach((key) => {
|
Object.keys(state).forEach((key) => {
|
||||||
if (key.startsWith("opt")) {
|
if (key.startsWith("opt")) {
|
||||||
(state)[key] = conf[key];
|
(state as any)[key] = conf[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -376,15 +370,13 @@ export default new Vuex.Store({
|
|||||||
},
|
},
|
||||||
modules: {},
|
modules: {},
|
||||||
getters: {
|
getters: {
|
||||||
nerModel: (state) => state.nerModel,
|
mlModel: (state) => state.mlModel,
|
||||||
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,
|
||||||
sist2Info: state => state.sist2Info,
|
sist2Info: state => state.sist2Info,
|
||||||
indexMap: state => {
|
indexMap: state => {
|
||||||
const map = {};
|
const map = {} as any;
|
||||||
state.indices.forEach(idx => map[idx.id] = idx);
|
state.indices.forEach(idx => map[idx.id] = idx);
|
||||||
return map;
|
return map;
|
||||||
},
|
},
|
||||||
@ -397,18 +389,17 @@ 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,
|
||||||
sortMode: state => state.sortMode,
|
sortMode: state => state.sortMode,
|
||||||
lastQueryResult: state => state.lastQueryResults,
|
lastQueryResult: state => state.lastQueryResults,
|
||||||
lastDoc: function (state) {
|
lastDoc: function (state): EsHit | null {
|
||||||
if (state.lastQueryResults == null) {
|
if (state.lastQueryResults == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (state.lastQueryResults).hits.hits.slice(-1)[0];
|
return (state.lastQueryResults as unknown as EsResult).hits.hits.slice(-1)[0];
|
||||||
},
|
},
|
||||||
uiShowLightbox: state => state.uiShowLightbox,
|
uiShowLightbox: state => state.uiShowLightbox,
|
||||||
uiLightboxSources: state => state.uiLightboxSources,
|
uiLightboxSources: state => state.uiLightboxSources,
|
||||||
@ -449,7 +440,7 @@ export default new Vuex.Store({
|
|||||||
optMlRepositories: state => state.optMlRepositories,
|
optMlRepositories: state => state.optMlRepositories,
|
||||||
mlRepositoryList: state => {
|
mlRepositoryList: state => {
|
||||||
const repos = state.optMlRepositories.split("\n")
|
const repos = state.optMlRepositories.split("\n")
|
||||||
return repos[0] === "" ? [] : repos;
|
return repos[0] == "" ? [] : repos;
|
||||||
},
|
},
|
||||||
optMlDefaultModel: state => state.optMlDefaultModel,
|
optMlDefaultModel: state => state.optMlDefaultModel,
|
||||||
optAutoAnalyze: state => state.optAutoAnalyze,
|
optAutoAnalyze: state => state.optAutoAnalyze,
|
139
sist2-vue/src/util-js.js
Normal file
139
sist2-vue/src/util-js.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
export function mergeTooltips(slider, threshold, separator, fixTooltips) {
|
||||||
|
|
||||||
|
const isMobile = window.innerWidth <= 650;
|
||||||
|
if (isMobile) {
|
||||||
|
threshold = 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textIsRtl = getComputedStyle(slider).direction === 'rtl';
|
||||||
|
const isRtl = slider.noUiSlider.options.direction === 'rtl';
|
||||||
|
const isVertical = slider.noUiSlider.options.orientation === 'vertical';
|
||||||
|
const tooltips = slider.noUiSlider.getTooltips();
|
||||||
|
const origins = slider.noUiSlider.getOrigins();
|
||||||
|
|
||||||
|
// Move tooltips into the origin element. The default stylesheet handles this.
|
||||||
|
tooltips.forEach(function (tooltip, index) {
|
||||||
|
if (tooltip) {
|
||||||
|
origins[index].appendChild(tooltip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
slider.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) {
|
||||||
|
|
||||||
|
const pools = [[]];
|
||||||
|
const poolPositions = [[]];
|
||||||
|
const poolValues = [[]];
|
||||||
|
let atPool = 0;
|
||||||
|
|
||||||
|
// Assign the first tooltip to the first pool, if the tooltip is configured
|
||||||
|
if (tooltips[0]) {
|
||||||
|
pools[0][0] = 0;
|
||||||
|
poolPositions[0][0] = positions[0];
|
||||||
|
poolValues[0][0] = values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < positions.length; i++) {
|
||||||
|
if (!tooltips[i] || (positions[i] - positions[i - 1]) > threshold) {
|
||||||
|
atPool++;
|
||||||
|
pools[atPool] = [];
|
||||||
|
poolValues[atPool] = [];
|
||||||
|
poolPositions[atPool] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tooltips[i]) {
|
||||||
|
pools[atPool].push(i);
|
||||||
|
poolValues[atPool].push(values[i]);
|
||||||
|
poolPositions[atPool].push(positions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pools.forEach(function (pool, poolIndex) {
|
||||||
|
const handlesInPool = pool.length;
|
||||||
|
|
||||||
|
for (let j = 0; j < handlesInPool; j++) {
|
||||||
|
const handleNumber = pool[j];
|
||||||
|
|
||||||
|
if (j === handlesInPool - 1) {
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
poolPositions[poolIndex].forEach(function (value) {
|
||||||
|
offset += 1000 - 10 * value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const direction = isVertical ? 'bottom' : 'right';
|
||||||
|
const last = isRtl ? 0 : handlesInPool - 1;
|
||||||
|
const lastOffset = 1000 - 10 * poolPositions[poolIndex][last];
|
||||||
|
offset = (textIsRtl && !isVertical ? 100 : 0) + (offset / handlesInPool) - lastOffset;
|
||||||
|
|
||||||
|
// Center this tooltip over the affected handles
|
||||||
|
tooltips[handleNumber].innerHTML = poolValues[poolIndex].join(separator);
|
||||||
|
tooltips[handleNumber].style.display = 'block';
|
||||||
|
|
||||||
|
tooltips[handleNumber].style[direction] = offset + '%';
|
||||||
|
} else {
|
||||||
|
// Hide this tooltip
|
||||||
|
tooltips[handleNumber].style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fixTooltips) {
|
||||||
|
const isMobile = window.innerWidth <= 650;
|
||||||
|
const len = isMobile ? 20 : 5;
|
||||||
|
|
||||||
|
if (positions[0] < len) {
|
||||||
|
tooltips[0].style.right = `${(1 - ((positions[0]) / len)) * -35}px`
|
||||||
|
} else {
|
||||||
|
tooltips[0].style.right = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positions[1] > (100 - len)) {
|
||||||
|
tooltips[1].style.right = `${((positions[1] - (100 - len)) / len) * 35}px`
|
||||||
|
} else {
|
||||||
|
tooltips[1].style.right = "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function burrow(table, addSelfDir, rootName) {
|
||||||
|
const root = {};
|
||||||
|
table.forEach(row => {
|
||||||
|
let layer = root;
|
||||||
|
|
||||||
|
row.taxonomy.forEach(key => {
|
||||||
|
layer[key] = key in layer ? layer[key] : {};
|
||||||
|
layer = layer[key];
|
||||||
|
});
|
||||||
|
if (Object.keys(layer).length === 0) {
|
||||||
|
layer["$size$"] = row.size;
|
||||||
|
} else if (addSelfDir) {
|
||||||
|
layer["."] = {
|
||||||
|
"$size$": row.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const descend = function (obj, depth) {
|
||||||
|
return Object.keys(obj).filter(k => k !== "$size$").map(k => {
|
||||||
|
const child = {
|
||||||
|
name: k,
|
||||||
|
depth: depth,
|
||||||
|
value: 0,
|
||||||
|
children: descend(obj[k], depth + 1)
|
||||||
|
};
|
||||||
|
if ("$size$" in obj[k]) {
|
||||||
|
child.value = obj[k]["$size$"];
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: rootName,
|
||||||
|
children: descend(root, 1),
|
||||||
|
value: 0,
|
||||||
|
depth: 0,
|
||||||
|
}
|
||||||
|
}
|
@ -1,329 +0,0 @@
|
|||||||
export function mergeTooltips(slider, threshold, separator, fixTooltips) {
|
|
||||||
|
|
||||||
const isMobile = window.innerWidth <= 650;
|
|
||||||
if (isMobile) {
|
|
||||||
threshold = 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
const textIsRtl = getComputedStyle(slider).direction === 'rtl';
|
|
||||||
const isRtl = slider.noUiSlider.options.direction === 'rtl';
|
|
||||||
const isVertical = slider.noUiSlider.options.orientation === 'vertical';
|
|
||||||
const tooltips = slider.noUiSlider.getTooltips();
|
|
||||||
const origins = slider.noUiSlider.getOrigins();
|
|
||||||
|
|
||||||
// Move tooltips into the origin element. The default stylesheet handles this.
|
|
||||||
tooltips.forEach(function (tooltip, index) {
|
|
||||||
if (tooltip) {
|
|
||||||
origins[index].appendChild(tooltip);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
slider.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) {
|
|
||||||
|
|
||||||
const pools = [[]];
|
|
||||||
const poolPositions = [[]];
|
|
||||||
const poolValues = [[]];
|
|
||||||
let atPool = 0;
|
|
||||||
|
|
||||||
// Assign the first tooltip to the first pool, if the tooltip is configured
|
|
||||||
if (tooltips[0]) {
|
|
||||||
pools[0][0] = 0;
|
|
||||||
poolPositions[0][0] = positions[0];
|
|
||||||
poolValues[0][0] = values[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i < positions.length; i++) {
|
|
||||||
if (!tooltips[i] || (positions[i] - positions[i - 1]) > threshold) {
|
|
||||||
atPool++;
|
|
||||||
pools[atPool] = [];
|
|
||||||
poolValues[atPool] = [];
|
|
||||||
poolPositions[atPool] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tooltips[i]) {
|
|
||||||
pools[atPool].push(i);
|
|
||||||
poolValues[atPool].push(values[i]);
|
|
||||||
poolPositions[atPool].push(positions[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pools.forEach(function (pool, poolIndex) {
|
|
||||||
const handlesInPool = pool.length;
|
|
||||||
|
|
||||||
for (let j = 0; j < handlesInPool; j++) {
|
|
||||||
const handleNumber = pool[j];
|
|
||||||
|
|
||||||
if (j === handlesInPool - 1) {
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
poolPositions[poolIndex].forEach(function (value) {
|
|
||||||
offset += 1000 - 10 * value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const direction = isVertical ? 'bottom' : 'right';
|
|
||||||
const last = isRtl ? 0 : handlesInPool - 1;
|
|
||||||
const lastOffset = 1000 - 10 * poolPositions[poolIndex][last];
|
|
||||||
offset = (textIsRtl && !isVertical ? 100 : 0) + (offset / handlesInPool) - lastOffset;
|
|
||||||
|
|
||||||
// Center this tooltip over the affected handles
|
|
||||||
tooltips[handleNumber].innerHTML = poolValues[poolIndex].join(separator);
|
|
||||||
tooltips[handleNumber].style.display = 'block';
|
|
||||||
|
|
||||||
tooltips[handleNumber].style[direction] = offset + '%';
|
|
||||||
} else {
|
|
||||||
// Hide this tooltip
|
|
||||||
tooltips[handleNumber].style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fixTooltips) {
|
|
||||||
const isMobile = window.innerWidth <= 650;
|
|
||||||
const len = isMobile ? 20 : 5;
|
|
||||||
|
|
||||||
if (positions[0] < len) {
|
|
||||||
tooltips[0].style.right = `${(1 - ((positions[0]) / len)) * -35}px`
|
|
||||||
} else {
|
|
||||||
tooltips[0].style.right = "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (positions[1] > (100 - len)) {
|
|
||||||
tooltips[1].style.right = `${((positions[1] - (100 - len)) / len) * 35}px`
|
|
||||||
} else {
|
|
||||||
tooltips[1].style.right = "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function burrow(table, addSelfDir, rootName) {
|
|
||||||
const root = {};
|
|
||||||
table.forEach(row => {
|
|
||||||
let layer = root;
|
|
||||||
|
|
||||||
row.taxonomy.forEach(key => {
|
|
||||||
layer[key] = key in layer ? layer[key] : {};
|
|
||||||
layer = layer[key];
|
|
||||||
});
|
|
||||||
if (Object.keys(layer).length === 0) {
|
|
||||||
layer["$size$"] = row.size;
|
|
||||||
} else if (addSelfDir) {
|
|
||||||
layer["."] = {
|
|
||||||
"$size$": row.size,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const descend = function (obj, depth) {
|
|
||||||
return Object.keys(obj).filter(k => k !== "$size$").map(k => {
|
|
||||||
const child = {
|
|
||||||
name: k,
|
|
||||||
depth: depth,
|
|
||||||
value: 0,
|
|
||||||
children: descend(obj[k], depth + 1)
|
|
||||||
};
|
|
||||||
if ("$size$" in obj[k]) {
|
|
||||||
child.value = obj[k]["$size$"];
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: rootName,
|
|
||||||
children: descend(root, 1),
|
|
||||||
value: 0,
|
|
||||||
depth: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function ext(hit) {
|
|
||||||
return srcExt(hit._source)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function srcExt(src) {
|
|
||||||
return Object.prototype.hasOwnProperty.call(src, "extension")
|
|
||||||
&& src["extension"] !== "" ? "." + src["extension"] : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function strUnescape(str) {
|
|
||||||
let result = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
const c = str[i];
|
|
||||||
const next = str[i + 1];
|
|
||||||
|
|
||||||
if (c === "]") {
|
|
||||||
if (next === "]") {
|
|
||||||
result += c;
|
|
||||||
i += 1;
|
|
||||||
} else {
|
|
||||||
result += String.fromCharCode(parseInt(str.slice(i, i + 2), 16));
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thresh = 1000;
|
|
||||||
const units = ["k", "M", "G", "T", "P", "E", "Z", "Y"];
|
|
||||||
|
|
||||||
export function humanFileSize(bytes) {
|
|
||||||
if (bytes === 0) {
|
|
||||||
return "0 B"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(bytes) < thresh) {
|
|
||||||
return bytes + ' B';
|
|
||||||
}
|
|
||||||
let u = -1;
|
|
||||||
do {
|
|
||||||
bytes /= thresh;
|
|
||||||
++u;
|
|
||||||
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
|
|
||||||
|
|
||||||
return bytes.toFixed(1) + units[u];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function humanTime(sec_num) {
|
|
||||||
sec_num = Math.floor(sec_num);
|
|
||||||
const hours = Math.floor(sec_num / 3600);
|
|
||||||
const minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
|
||||||
const seconds = sec_num - (hours * 3600) - (minutes * 60);
|
|
||||||
|
|
||||||
if (sec_num < 60) {
|
|
||||||
return `${sec_num}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sec_num < 3600) {
|
|
||||||
return `${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${hours < 10 ? "0" : ""}${hours}:${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function humanDate(numMilis) {
|
|
||||||
const date = (new Date(numMilis * 1000));
|
|
||||||
return date.getUTCFullYear() + "-" + ("0" + (date.getUTCMonth() + 1)).slice(-2) + "-" + ("0" + date.getUTCDate()).slice(-2)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function lum(c) {
|
|
||||||
c = c.substring(1);
|
|
||||||
const rgb = parseInt(c, 16);
|
|
||||||
const r = (rgb >> 16) & 0xff;
|
|
||||||
const g = (rgb >> 8) & 0xff;
|
|
||||||
const b = (rgb >> 0) & 0xff;
|
|
||||||
|
|
||||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function getSelectedTreeNodes(tree) {
|
|
||||||
const selectedNodes = new Set();
|
|
||||||
|
|
||||||
const selected = tree.selected();
|
|
||||||
|
|
||||||
for (let i = 0; i < selected.length; i++) {
|
|
||||||
|
|
||||||
if (selected[i].id === "any") {
|
|
||||||
return ["any"]
|
|
||||||
}
|
|
||||||
|
|
||||||
//Only get children
|
|
||||||
if (selected[i].text.indexOf("(") !== -1) {
|
|
||||||
if (selected[i].values) {
|
|
||||||
selectedNodes.add(selected[i].values.slice(-1)[0]);
|
|
||||||
} else {
|
|
||||||
selectedNodes.add(selected[i].id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(selectedNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTreeNodeAttributes(tree) {
|
|
||||||
const nodes = tree.selectable();
|
|
||||||
const attributes = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
|
||||||
|
|
||||||
let id = null;
|
|
||||||
|
|
||||||
if (nodes[i].text.indexOf("(") !== -1 && nodes[i].values) {
|
|
||||||
id = nodes[i].values.slice(-1)[0];
|
|
||||||
} else {
|
|
||||||
id = nodes[i].id
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes[id] = {
|
|
||||||
checked: nodes[i].itree.state.checked,
|
|
||||||
collapsed: nodes[i].itree.state.collapsed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function serializeMimes(mimes) {
|
|
||||||
if (mimes.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return mimes.map(mime => compressMime(mime)).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deserializeMimes(mimeString) {
|
|
||||||
return mimeString
|
|
||||||
.replaceAll(/([IVATUF])/g, "$$$&")
|
|
||||||
.split("$")
|
|
||||||
.map(mime => decompressMime(mime))
|
|
||||||
.slice(1) // Ignore the first (empty) token
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compressMime(mime) {
|
|
||||||
return mime
|
|
||||||
.replace("image/", "I")
|
|
||||||
.replace("video/", "V")
|
|
||||||
.replace("application/", "A")
|
|
||||||
.replace("text/", "T")
|
|
||||||
.replace("audio/", "U")
|
|
||||||
.replace("font/", "F")
|
|
||||||
.replace("+", ",")
|
|
||||||
.replace("x-", "X")
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decompressMime(mime) {
|
|
||||||
return mime
|
|
||||||
.replace("I", "image/")
|
|
||||||
.replace("V", "video/")
|
|
||||||
.replace("A", "application/")
|
|
||||||
.replace("T", "text/")
|
|
||||||
.replace("U", "audio/")
|
|
||||||
.replace("F", "font/")
|
|
||||||
.replace(",", "+")
|
|
||||||
.replace("X", "x-")
|
|
||||||
}
|
|
||||||
|
|
||||||
export function randomSeed() {
|
|
||||||
return Math.round(Math.random() * 100000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sid(doc) {
|
|
||||||
if (doc._id.includes(".")) {
|
|
||||||
return doc._id
|
|
||||||
}
|
|
||||||
|
|
||||||
const num = BigInt(doc._id);
|
|
||||||
|
|
||||||
const indexId = (num >> BigInt(32));
|
|
||||||
const docId = num & BigInt(0xFFFFFFFF);
|
|
||||||
|
|
||||||
return indexId.toString(16).padStart(8, "0") + "." + docId.toString(16).padStart(8, "0");
|
|
||||||
}
|
|
177
sist2-vue/src/util.ts
Normal file
177
sist2-vue/src/util.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import {EsHit} from "@/Sist2Api";
|
||||||
|
|
||||||
|
export function ext(hit: EsHit) {
|
||||||
|
return srcExt(hit._source)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function srcExt(src) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(src, "extension")
|
||||||
|
&& src["extension"] !== "" ? "." + src["extension"] : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function strUnescape(str: string): string {
|
||||||
|
let result = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const c = str[i];
|
||||||
|
const next = str[i + 1];
|
||||||
|
|
||||||
|
if (c === "]") {
|
||||||
|
if (next === "]") {
|
||||||
|
result += c;
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
result += String.fromCharCode(parseInt(str.slice(i, i + 2), 16));
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thresh = 1000;
|
||||||
|
const units = ["k", "M", "G", "T", "P", "E", "Z", "Y"];
|
||||||
|
|
||||||
|
export function humanFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) {
|
||||||
|
return "0 B"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(bytes) < thresh) {
|
||||||
|
return bytes + ' B';
|
||||||
|
}
|
||||||
|
let u = -1;
|
||||||
|
do {
|
||||||
|
bytes /= thresh;
|
||||||
|
++u;
|
||||||
|
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
|
||||||
|
|
||||||
|
return bytes.toFixed(1) + units[u];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanTime(sec_num: number): string {
|
||||||
|
sec_num = Math.floor(sec_num);
|
||||||
|
const hours = Math.floor(sec_num / 3600);
|
||||||
|
const minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||||
|
const seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||||
|
|
||||||
|
if (sec_num < 60) {
|
||||||
|
return `${sec_num}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sec_num < 3600) {
|
||||||
|
return `${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${hours < 10 ? "0" : ""}${hours}:${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanDate(numMilis: number): string {
|
||||||
|
const date = (new Date(numMilis * 1000));
|
||||||
|
return date.getUTCFullYear() + "-" + ("0" + (date.getUTCMonth() + 1)).slice(-2) + "-" + ("0" + date.getUTCDate()).slice(-2)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lum(c: string) {
|
||||||
|
c = c.substring(1);
|
||||||
|
const rgb = parseInt(c, 16);
|
||||||
|
const r = (rgb >> 16) & 0xff;
|
||||||
|
const g = (rgb >> 8) & 0xff;
|
||||||
|
const b = (rgb >> 0) & 0xff;
|
||||||
|
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getSelectedTreeNodes(tree: any) {
|
||||||
|
const selectedNodes = new Set();
|
||||||
|
|
||||||
|
const selected = tree.selected();
|
||||||
|
|
||||||
|
for (let i = 0; i < selected.length; i++) {
|
||||||
|
|
||||||
|
if (selected[i].id === "any") {
|
||||||
|
return ["any"]
|
||||||
|
}
|
||||||
|
|
||||||
|
//Only get children
|
||||||
|
if (selected[i].text.indexOf("(") !== -1) {
|
||||||
|
if (selected[i].values) {
|
||||||
|
selectedNodes.add(selected[i].values.slice(-1)[0]);
|
||||||
|
} else {
|
||||||
|
selectedNodes.add(selected[i].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(selectedNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTreeNodeAttributes(tree: any) {
|
||||||
|
const nodes = tree.selectable();
|
||||||
|
const attributes = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
|
||||||
|
let id = null;
|
||||||
|
|
||||||
|
if (nodes[i].text.indexOf("(") !== -1 && nodes[i].values) {
|
||||||
|
id = nodes[i].values.slice(-1)[0];
|
||||||
|
} else {
|
||||||
|
id = nodes[i].id
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes[id] = {
|
||||||
|
checked: nodes[i].itree.state.checked,
|
||||||
|
collapsed: nodes[i].itree.state.collapsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function serializeMimes(mimes: string[]): string | undefined {
|
||||||
|
if (mimes.length == 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return mimes.map(mime => compressMime(mime)).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deserializeMimes(mimeString: string): string[] {
|
||||||
|
return mimeString
|
||||||
|
.replaceAll(/([IVATUF])/g, "$$$&")
|
||||||
|
.split("$")
|
||||||
|
.map(mime => decompressMime(mime))
|
||||||
|
.slice(1) // Ignore the first (empty) token
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compressMime(mime: string): string {
|
||||||
|
return mime
|
||||||
|
.replace("image/", "I")
|
||||||
|
.replace("video/", "V")
|
||||||
|
.replace("application/", "A")
|
||||||
|
.replace("text/", "T")
|
||||||
|
.replace("audio/", "U")
|
||||||
|
.replace("font/", "F")
|
||||||
|
.replace("+", ",")
|
||||||
|
.replace("x-", "X")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decompressMime(mime: string): string {
|
||||||
|
return mime
|
||||||
|
.replace("I", "image/")
|
||||||
|
.replace("V", "video/")
|
||||||
|
.replace("A", "application/")
|
||||||
|
.replace("T", "text/")
|
||||||
|
.replace("U", "audio/")
|
||||||
|
.replace("F", "font/")
|
||||||
|
.replace(",", "+")
|
||||||
|
.replace("X", "x-")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomSeed(): number {
|
||||||
|
return Math.round(Math.random() * 100000);
|
||||||
|
}
|
@ -81,7 +81,6 @@
|
|||||||
<li><code>doc.artist</code></li>
|
<li><code>doc.artist</code></li>
|
||||||
<li><code>doc.title</code></li>
|
<li><code>doc.title</code></li>
|
||||||
<li><code>doc.genre</code></li>
|
<li><code>doc.genre</code></li>
|
||||||
<li><code>doc.media_comment</code></li>
|
|
||||||
<li><code>doc.album_artist</code></li>
|
<li><code>doc.album_artist</code></li>
|
||||||
<li><code>doc.exif_make</code></li>
|
<li><code>doc.exif_make</code></li>
|
||||||
<li><code>doc.exif_model</code></li>
|
<li><code>doc.exif_model</code></li>
|
||||||
@ -137,7 +136,7 @@
|
|||||||
{{ $t("opt.fuzzy") }}
|
{{ $t("opt.fuzzy") }}
|
||||||
</b-form-checkbox>
|
</b-form-checkbox>
|
||||||
|
|
||||||
<b-form-checkbox :checked="optSearchInPath" @input="setOptSearchInPath">{{
|
<b-form-checkbox :disabled="uiSqliteMode" :checked="optSearchInPath" @input="setOptSearchInPath">{{
|
||||||
$t("opt.searchInPath")
|
$t("opt.searchInPath")
|
||||||
}}
|
}}
|
||||||
</b-form-checkbox>
|
</b-form-checkbox>
|
||||||
|
@ -1,142 +1,142 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="margin-left: auto; margin-right: auto;" class="container">
|
<div style="margin-left: auto; margin-right: auto;" class="container">
|
||||||
<Preloader v-if="loading"></Preloader>
|
<Preloader v-if="loading"></Preloader>
|
||||||
<b-card v-else-if="!loading && found">
|
<b-card v-else-if="!loading && found">
|
||||||
<b-card-title :title="doc._source.name + ext(doc)">
|
<b-card-title :title="doc._source.name + ext(doc)">
|
||||||
{{ doc._source.name + ext(doc) }}
|
{{ doc._source.name + ext(doc) }}
|
||||||
</b-card-title>
|
</b-card-title>
|
||||||
|
|
||||||
<!-- Thumbnail-->
|
<!-- Thumbnail-->
|
||||||
<div style="position: relative; margin-left: auto; margin-right: auto; text-align: center">
|
<div style="position: relative; margin-left: auto; margin-right: auto; text-align: center">
|
||||||
<FullThumbnail :doc="doc" :small-badge="false" @onThumbnailClick="onThumbnailClick()"></FullThumbnail>
|
<FullThumbnail :doc="doc" :small-badge="false" @onThumbnailClick="onThumbnailClick()"></FullThumbnail>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 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/${sid(doc)}`"></audio>
|
:src="`f/${doc._source.index}/${doc._id}`"></audio>
|
||||||
|
|
||||||
<InfoTable :doc="doc" v-if="doc"></InfoTable>
|
<InfoTable :doc="doc" v-if="doc"></InfoTable>
|
||||||
|
|
||||||
<div v-if="doc._source.content" class="content-div">{{ doc._source.content }}</div>
|
<div v-if="doc._source.content" class="content-div">{{ doc._source.content }}</div>
|
||||||
</b-card>
|
</b-card>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<b-card>
|
<b-card>
|
||||||
<b-card-title>{{ $t("filePage.notFound") }}</b-card-title>
|
<b-card-title>{{ $t("filePage.notFound") }}</b-card-title>
|
||||||
</b-card>
|
</b-card>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Preloader from "@/components/Preloader.vue";
|
import Preloader from "@/components/Preloader.vue";
|
||||||
import InfoTable from "@/components/InfoTable.vue";
|
import InfoTable from "@/components/InfoTable.vue";
|
||||||
import Sist2Api from "@/Sist2Api";
|
import Sist2Api from "@/Sist2Api";
|
||||||
import {ext, sid} from "@/util";
|
import {ext} from "@/util";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
|
import sist2 from "@/Sist2Api";
|
||||||
import FullThumbnail from "@/components/FullThumbnail";
|
import FullThumbnail from "@/components/FullThumbnail";
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: "FilePage",
|
name: "FilePage",
|
||||||
components: {
|
components: {
|
||||||
FullThumbnail,
|
FullThumbnail,
|
||||||
Preloader,
|
Preloader,
|
||||||
InfoTable
|
InfoTable
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
found: false,
|
found: false,
|
||||||
doc: null
|
doc: null
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
ext: ext,
|
|
||||||
sid: sid,
|
|
||||||
onThumbnailClick() {
|
|
||||||
window.open(`/f/${sid(this.doc)}`, "_blank");
|
|
||||||
},
|
|
||||||
findByCustomField(field, id) {
|
|
||||||
return {
|
|
||||||
query: {
|
|
||||||
bool: {
|
|
||||||
must: [
|
|
||||||
{
|
|
||||||
match: {
|
|
||||||
[field]: id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
size: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
findById(id) {
|
|
||||||
return {
|
|
||||||
query: {
|
|
||||||
bool: {
|
|
||||||
must: [
|
|
||||||
{
|
|
||||||
match: {
|
|
||||||
"_id": id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
size: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
findByName(name) {
|
|
||||||
return {
|
|
||||||
query: {
|
|
||||||
bool: {
|
|
||||||
must: [
|
|
||||||
{
|
|
||||||
match: {
|
|
||||||
"name": name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
size: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
let query = null;
|
|
||||||
if (this.$route.query.byId) {
|
|
||||||
query = this.findById(this.$route.query.byId);
|
|
||||||
} else if (this.$route.query.byName) {
|
|
||||||
query = this.findByName(this.$route.query.byName);
|
|
||||||
} else if (this.$route.query.by && this.$route.query.q) {
|
|
||||||
query = this.findByCustomField(this.$route.query.by, this.$route.query.q)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query) {
|
|
||||||
Sist2Api.esQuery(query).then(result => {
|
|
||||||
if (result.hits.hits.length === 0) {
|
|
||||||
this.found = false;
|
|
||||||
} else {
|
|
||||||
this.doc = result.hits.hits[0];
|
|
||||||
this.found = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.loading = false;
|
|
||||||
this.found = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
ext: ext,
|
||||||
|
onThumbnailClick() {
|
||||||
|
window.open(`/f/${this.doc.index}/${this.doc._id}`, "_blank");
|
||||||
|
},
|
||||||
|
findByCustomField(field, id) {
|
||||||
|
return {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
must: [
|
||||||
|
{
|
||||||
|
match: {
|
||||||
|
[field]: id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
findById(id) {
|
||||||
|
return {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
must: [
|
||||||
|
{
|
||||||
|
match: {
|
||||||
|
"_id": id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
findByName(name) {
|
||||||
|
return {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
must: [
|
||||||
|
{
|
||||||
|
match: {
|
||||||
|
"name": name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
let query = null;
|
||||||
|
if (this.$route.query.byId) {
|
||||||
|
query = this.findById(this.$route.query.byId);
|
||||||
|
} else if (this.$route.query.byName) {
|
||||||
|
query = this.findByName(this.$route.query.byName);
|
||||||
|
} else if (this.$route.query.by && this.$route.query.q) {
|
||||||
|
query = this.findByCustomField(this.$route.query.by, this.$route.query.q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
Sist2Api.esQuery(query).then(result => {
|
||||||
|
if (result.hits.hits.length === 0) {
|
||||||
|
this.found = false;
|
||||||
|
} else {
|
||||||
|
this.doc = result.hits.hits[0];
|
||||||
|
this.found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.loading = false;
|
||||||
|
this.found = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.img-wrapper {
|
.img-wrapper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
<b-card v-show="!uiLoading && !showEsConnectionError" id="search-panel">
|
<b-card v-show="!uiLoading && !showEsConnectionError" id="search-panel">
|
||||||
<SearchBar @show-help="showHelp=true"></SearchBar>
|
<SearchBar @show-help="showHelp=true"></SearchBar>
|
||||||
<EmbeddingsSearchBar v-if="hasEmbeddings" class="mt-3"></EmbeddingsSearchBar>
|
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-col style="height: 70px;" sm="6">
|
<b-col style="height: 70px;" sm="6">
|
||||||
<SizeSlider></SizeSlider>
|
<SizeSlider></SizeSlider>
|
||||||
@ -59,15 +58,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import {sid} from "@/util";
|
|
||||||
import Preloader from "@/components/Preloader.vue";
|
import Preloader from "@/components/Preloader.vue";
|
||||||
import {mapActions, mapGetters, mapMutations} from "vuex";
|
import {mapActions, mapGetters, mapMutations} from "vuex";
|
||||||
|
import sist2 from "../Sist2Api";
|
||||||
|
import Sist2Api, {EsHit, EsResult} from "../Sist2Api";
|
||||||
import SearchBar from "@/components/SearchBar.vue";
|
import SearchBar from "@/components/SearchBar.vue";
|
||||||
import IndexPicker from "@/components/IndexPicker.vue";
|
import IndexPicker from "@/components/IndexPicker.vue";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
||||||
import {debounce as _debounce} from "underscore";
|
import _debounce from "lodash/debounce";
|
||||||
import DocCardWall from "@/components/DocCardWall.vue";
|
import DocCardWall from "@/components/DocCardWall.vue";
|
||||||
import Lightbox from "@/components/Lightbox.vue";
|
import Lightbox from "@/components/Lightbox.vue";
|
||||||
import LightboxCaption from "@/components/LightboxCaption.vue";
|
import LightboxCaption from "@/components/LightboxCaption.vue";
|
||||||
@ -79,13 +79,11 @@ import DateSlider from "@/components/DateSlider.vue";
|
|||||||
import TagPicker from "@/components/TagPicker.vue";
|
import TagPicker from "@/components/TagPicker.vue";
|
||||||
import DocList from "@/components/DocList.vue";
|
import DocList from "@/components/DocList.vue";
|
||||||
import HelpDialog from "@/components/HelpDialog.vue";
|
import HelpDialog from "@/components/HelpDialog.vue";
|
||||||
import EmbeddingsSearchBar from "@/components/EmbeddingsSearchBar.vue";
|
import Sist2SqliteQuery from "@/Sist2SqliteQuery";
|
||||||
import Sist2Api from "@/Sist2Api";
|
|
||||||
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
EmbeddingsSearchBar,
|
|
||||||
HelpDialog,
|
HelpDialog,
|
||||||
DocList,
|
DocList,
|
||||||
TagPicker,
|
TagPicker,
|
||||||
@ -93,9 +91,10 @@ export default Vue.extend({
|
|||||||
SizeSlider, PathTree, ResultsCard, MimePicker, Lightbox, DocCardWall, IndexPicker, SearchBar, Preloader
|
SizeSlider, PathTree, ResultsCard, MimePicker, Lightbox, DocCardWall, IndexPicker, SearchBar, Preloader
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
loading: false,
|
||||||
uiLoading: true,
|
uiLoading: true,
|
||||||
search: undefined,
|
search: undefined as any,
|
||||||
docs: [],
|
docs: [] as EsHit[],
|
||||||
docIds: new Set(),
|
docIds: new Set(),
|
||||||
docChecksums: new Set(),
|
docChecksums: new Set(),
|
||||||
searchBusy: false,
|
searchBusy: false,
|
||||||
@ -105,23 +104,20 @@ export default Vue.extend({
|
|||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(["indices", "optDisplay"]),
|
...mapGetters(["indices", "optDisplay"]),
|
||||||
hasEmbeddings() {
|
|
||||||
return Sist2Api.models().length > 0;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// Handle touch events
|
// Handle touch events
|
||||||
window.ontouchend = () => this.$store.commit("busTouchEnd");
|
window.ontouchend = () => this.$store.commit("busTouchEnd");
|
||||||
window.ontouchcancel = () => this.$store.commit("busTouchEnd");
|
window.ontouchcancel = this.$store.commit("busTouchEnd");
|
||||||
|
|
||||||
this.search = _debounce(async (clear) => {
|
this.search = _debounce(async (clear: boolean) => {
|
||||||
if (clear) {
|
if (clear) {
|
||||||
await this.clearResults();
|
await this.clearResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.searchNow();
|
await this.searchNow();
|
||||||
|
|
||||||
}, 350, false);
|
}, 350, {leading: false});
|
||||||
|
|
||||||
this.$store.dispatch("loadFromArgs", this.$route).then(() => {
|
this.$store.dispatch("loadFromArgs", this.$route).then(() => {
|
||||||
this.$store.subscribe(() => this.$store.dispatch("updateArgs", this.$router));
|
this.$store.subscribe(() => this.$store.dispatch("updateArgs", this.$router));
|
||||||
@ -130,7 +126,6 @@ export default Vue.extend({
|
|||||||
"setSizeMin", "setSizeMax", "setDateMin", "setDateMax", "setSearchText", "setPathText",
|
"setSizeMin", "setSizeMax", "setDateMin", "setDateMax", "setSearchText", "setPathText",
|
||||||
"setSortMode", "setOptHighlight", "setOptFragmentSize", "setFuzzy", "setSize", "setSelectedIndices",
|
"setSortMode", "setOptHighlight", "setOptFragmentSize", "setFuzzy", "setSize", "setSelectedIndices",
|
||||||
"setSelectedMimeTypes", "setSelectedTags", "setOptQueryMode", "setOptSearchInPath",
|
"setSelectedMimeTypes", "setSelectedTags", "setOptQueryMode", "setOptSearchInPath",
|
||||||
"setEmbedding"
|
|
||||||
].includes(mutation.type)) {
|
].includes(mutation.type)) {
|
||||||
if (this.searchBusy) {
|
if (this.searchBusy) {
|
||||||
return;
|
return;
|
||||||
@ -157,7 +152,7 @@ export default Vue.extend({
|
|||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
||||||
if (error.response.status === 503 || error.response.status === 500) {
|
if (error.response.status == 503 || error.response.status == 500) {
|
||||||
this.showEsConnectionError = true;
|
this.showEsConnectionError = true;
|
||||||
this.uiLoading = false;
|
this.uiLoading = false;
|
||||||
} else {
|
} else {
|
||||||
@ -186,7 +181,7 @@ export default Vue.extend({
|
|||||||
bodyClass: "toast-body-error",
|
bodyClass: "toast-body-error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
showSyntaxErrorToast: function () {
|
showSyntaxErrorToast: function (): void {
|
||||||
this.$bvToast.toast(
|
this.$bvToast.toast(
|
||||||
this.$t("toast.esQueryErr"),
|
this.$t("toast.esQueryErr"),
|
||||||
{
|
{
|
||||||
@ -202,11 +197,10 @@ export default Vue.extend({
|
|||||||
await this.$store.dispatch("incrementQuerySequence");
|
await this.$store.dispatch("incrementQuerySequence");
|
||||||
this.$store.commit("busSearch");
|
this.$store.commit("busSearch");
|
||||||
|
|
||||||
Sist2Api.search().then(async (resp) => {
|
Sist2Api.search().then(async (resp: EsResult) => {
|
||||||
await this.handleSearch(resp);
|
await this.handleSearch(resp);
|
||||||
this.searchBusy = false;
|
this.searchBusy = false;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.log(err)
|
|
||||||
if (err.response.status === 500 && this.$store.state.optQueryMode === "advanced") {
|
if (err.response.status === 500 && this.$store.state.optQueryMode === "advanced") {
|
||||||
this.showSyntaxErrorToast();
|
this.showSyntaxErrorToast();
|
||||||
} else {
|
} else {
|
||||||
@ -221,8 +215,8 @@ export default Vue.extend({
|
|||||||
await this.$store.dispatch("clearResults");
|
await this.$store.dispatch("clearResults");
|
||||||
this.$store.commit("setUiReachedScrollEnd", false);
|
this.$store.commit("setUiReachedScrollEnd", false);
|
||||||
},
|
},
|
||||||
async handleSearch(resp) {
|
async handleSearch(resp: EsResult) {
|
||||||
if (resp.hits.hits.length === 0 || resp.hits.hits.length < this.$store.state.optSize) {
|
if (resp.hits.hits.length == 0 || resp.hits.hits.length < this.$store.state.optSize) {
|
||||||
this.$store.commit("setUiReachedScrollEnd", true);
|
this.$store.commit("setUiReachedScrollEnd", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,9 +239,9 @@ export default Vue.extend({
|
|||||||
if (hit._props.isPlayableImage || hit._props.isPlayableVideo) {
|
if (hit._props.isPlayableImage || hit._props.isPlayableVideo) {
|
||||||
hit._seq = await this.$store.dispatch("getKeySequence");
|
hit._seq = await this.$store.dispatch("getKeySequence");
|
||||||
this.$store.commit("addLightboxSource", {
|
this.$store.commit("addLightboxSource", {
|
||||||
source: `f/${sid(hit)}`,
|
source: `f/${hit._source.index}/${hit._id}`,
|
||||||
thumbnail_count: hit._props.hasThumbnail
|
thumbnail: hit._props.hasThumbnail
|
||||||
? `t/${sid(hit)}`
|
? `t/${hit._source.index}/${hit._id}`
|
||||||
: null,
|
: null,
|
||||||
caption: {
|
caption: {
|
||||||
component: LightboxCaption,
|
component: LightboxCaption,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user