Repository: redis/RedisDesktopManager Branch: 2022 Commit: 15f6d85528cd Files: 337 Total size: 1.6 MB Directory structure: gitextract_g8d5gff1/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── run_tests.yml │ └── sonar.yml ├── .gitignore ├── .gitmodules ├── .readthedocs.yaml ├── 3rdparty/ │ ├── 3rdparty.pri │ └── pyotherside.pri ├── BACKERS.md ├── CONTRIBUTING.md ├── COPYRIGHT ├── LICENSE ├── README.md ├── build/ │ └── windows/ │ └── installer/ │ ├── include/ │ │ ├── install_vcredist_x64.nsh │ │ ├── nsProcess.nsh │ │ └── x64.nsh │ ├── installer.nsi │ └── resources/ │ └── qt.conf ├── docs/ │ ├── app-store.md │ ├── bulk-operations.md │ ├── css/ │ │ └── extra.css │ ├── development.md │ ├── extension-server.md │ ├── faq.md │ ├── index.md │ ├── install.md │ ├── known-issues.md │ ├── lg-keyspaces.md │ ├── native-formatters.md │ ├── quick-start.md │ ├── requirements.txt │ └── server_spec.yaml ├── mkdocs.yml ├── sonar-project.properties ├── src/ │ ├── app/ │ │ ├── app.cpp │ │ ├── app.h │ │ ├── apputils.h │ │ ├── darkmode.h │ │ ├── events.cpp │ │ ├── events.h │ │ ├── jsonutils.cpp │ │ ├── jsonutils.h │ │ ├── models/ │ │ │ ├── configmanager.cpp │ │ │ ├── configmanager.h │ │ │ ├── connectionconf.cpp │ │ │ ├── connectionconf.h │ │ │ ├── connectiongroup.cpp │ │ │ ├── connectiongroup.h │ │ │ ├── connectionsmanager.cpp │ │ │ ├── connectionsmanager.h │ │ │ ├── key-models/ │ │ │ │ ├── abstractkey.h │ │ │ │ ├── bfkey.cpp │ │ │ │ ├── bfkey.h │ │ │ │ ├── hashkey.cpp │ │ │ │ ├── hashkey.h │ │ │ │ ├── keyfactory.cpp │ │ │ │ ├── keyfactory.h │ │ │ │ ├── listkey.cpp │ │ │ │ ├── listkey.h │ │ │ │ ├── listlikekey.cpp │ │ │ │ ├── listlikekey.h │ │ │ │ ├── newkeyrequest.cpp │ │ │ │ ├── newkeyrequest.h │ │ │ │ ├── rejsonkey.cpp │ │ │ │ ├── rejsonkey.h │ │ │ │ ├── rowcache.h │ │ │ │ ├── setkey.cpp │ │ │ │ ├── setkey.h │ │ │ │ ├── sortedsetkey.cpp │ │ │ │ ├── sortedsetkey.h │ │ │ │ ├── stream.cpp │ │ │ │ ├── stream.h │ │ │ │ ├── stringkey.cpp │ │ │ │ ├── stringkey.h │ │ │ │ ├── unknownkey.cpp │ │ │ │ └── unknownkey.h │ │ │ ├── treeoperations.cpp │ │ │ └── treeoperations.h │ │ ├── qcompress.cpp │ │ ├── qcompress.h │ │ ├── qmlutils.cpp │ │ └── qmlutils.h │ ├── main.cpp │ ├── modules/ │ │ ├── bulk-operations/ │ │ │ ├── bulkoperationsmanager.cpp │ │ │ ├── bulkoperationsmanager.h │ │ │ ├── connections.h │ │ │ └── operations/ │ │ │ ├── abstractoperation.cpp │ │ │ ├── abstractoperation.h │ │ │ ├── copyoperation.cpp │ │ │ ├── copyoperation.h │ │ │ ├── deleteoperation.cpp │ │ │ ├── deleteoperation.h │ │ │ ├── rdbimport.cpp │ │ │ ├── rdbimport.h │ │ │ ├── ttloperation.cpp │ │ │ └── ttloperation.h │ │ ├── common/ │ │ │ ├── baselistmodel.cpp │ │ │ ├── baselistmodel.h │ │ │ ├── callbackwithowner.h │ │ │ ├── sortfilterproxymodel.cpp │ │ │ ├── sortfilterproxymodel.h │ │ │ ├── tabmodel.cpp │ │ │ ├── tabmodel.h │ │ │ ├── tabviewmodel.cpp │ │ │ └── tabviewmodel.h │ │ ├── connections-tree/ │ │ │ ├── items/ │ │ │ │ ├── abstractnamespaceitem.cpp │ │ │ │ ├── abstractnamespaceitem.h │ │ │ │ ├── databaseitem.cpp │ │ │ │ ├── databaseitem.h │ │ │ │ ├── keyitem.cpp │ │ │ │ ├── keyitem.h │ │ │ │ ├── loadmoreitem.cpp │ │ │ │ ├── loadmoreitem.h │ │ │ │ ├── memoryusage.h │ │ │ │ ├── namespaceitem.cpp │ │ │ │ ├── namespaceitem.h │ │ │ │ ├── servergroup.cpp │ │ │ │ ├── servergroup.h │ │ │ │ ├── serveritem.cpp │ │ │ │ ├── serveritem.h │ │ │ │ ├── sortabletreeitem.h │ │ │ │ ├── treeitem.cpp │ │ │ │ └── treeitem.h │ │ │ ├── keysrendering.cpp │ │ │ ├── keysrendering.h │ │ │ ├── model.cpp │ │ │ ├── model.h │ │ │ ├── operations.h │ │ │ ├── utils.cpp │ │ │ └── utils.h │ │ ├── console/ │ │ │ ├── autocompletemodel.cpp │ │ │ ├── autocompletemodel.h │ │ │ ├── consolemodel.cpp │ │ │ └── consolemodel.h │ │ ├── exception.h │ │ ├── extension-server/ │ │ │ ├── client/ │ │ │ │ ├── CMakeLists.txt │ │ │ │ ├── OAIDataFormatter.cpp │ │ │ │ ├── OAIDataFormatter.h │ │ │ │ ├── OAIDecodePayload.cpp │ │ │ │ ├── OAIDecodePayload.h │ │ │ │ ├── OAIDefaultApi.cpp │ │ │ │ ├── OAIDefaultApi.h │ │ │ │ ├── OAIEncodePayload.cpp │ │ │ │ ├── OAIEncodePayload.h │ │ │ │ ├── OAIEnum.h │ │ │ │ ├── OAIHelpers.cpp │ │ │ │ ├── OAIHelpers.h │ │ │ │ ├── OAIHttpFileElement.cpp │ │ │ │ ├── OAIHttpFileElement.h │ │ │ │ ├── OAIHttpRequest.cpp │ │ │ │ ├── OAIHttpRequest.h │ │ │ │ ├── OAIInline_response_400.cpp │ │ │ │ ├── OAIInline_response_400.h │ │ │ │ ├── OAIOauth.cpp │ │ │ │ ├── OAIOauth.h │ │ │ │ ├── OAIObject.h │ │ │ │ ├── OAIServerConfiguration.h │ │ │ │ ├── OAIServerVariable.h │ │ │ │ └── client.pri │ │ │ ├── dataformattermanager.cpp │ │ │ ├── dataformattermanager.h │ │ │ └── generate_client.sh │ │ ├── server-actions/ │ │ │ ├── serverstatsmodel.cpp │ │ │ └── serverstatsmodel.h │ │ └── value-editor/ │ │ ├── abstractkeyfactory.h │ │ ├── embeddedformattersmanager.cpp │ │ ├── embeddedformattersmanager.h │ │ ├── keymodel.h │ │ ├── largetextmodel.cpp │ │ ├── largetextmodel.h │ │ ├── syntaxhighlighter.cpp │ │ ├── syntaxhighlighter.h │ │ ├── tabsmodel.cpp │ │ ├── tabsmodel.h │ │ ├── textcharformat.cpp │ │ ├── textcharformat.h │ │ ├── valueviewmodel.cpp │ │ └── valueviewmodel.h │ ├── py/ │ │ ├── formatters/ │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── binary.py │ │ │ ├── cbor.py │ │ │ ├── msgpack.py │ │ │ ├── phpserialize.py │ │ │ └── pickle.py │ │ ├── py.qrc │ │ ├── rdb/ │ │ │ └── __init__.py │ │ └── requirements.txt │ ├── qml/ │ │ ├── AppToolBar.qml │ │ ├── LogView.qml │ │ ├── QuickStartDialog.qml │ │ ├── WelcomeTab.qml │ │ ├── app.qml │ │ ├── bulk-operations/ │ │ │ └── BulkOperationsDialog.qml │ │ ├── common/ │ │ │ ├── AddressInput.qml │ │ │ ├── BetterButton.qml │ │ │ ├── BetterCheckbox.qml │ │ │ ├── BetterComboBox.qml │ │ │ ├── BetterDialog.qml │ │ │ ├── BetterDialogButtonBox.qml │ │ │ ├── BetterGroupbox.qml │ │ │ ├── BetterLabel.qml │ │ │ ├── BetterMenu.qml │ │ │ ├── BetterMenuItem.qml │ │ │ ├── BetterMessageDialog.qml │ │ │ ├── BetterRadioButton.qml │ │ │ ├── BetterSpinBox.qml │ │ │ ├── BetterSplitView.qml │ │ │ ├── BetterTab.qml │ │ │ ├── BetterTabButton.qml │ │ │ ├── BetterTabView.qml │ │ │ ├── BetterTextField.qml │ │ │ ├── BetterToolTip.qml │ │ │ ├── ColorInput.qml │ │ │ ├── FastTextView.qml │ │ │ ├── FilePathInput.qml │ │ │ ├── ImageButton.qml │ │ │ ├── JsonHighlighter.qml │ │ │ ├── LegacyTableView.qml │ │ │ ├── NewTextArea.qml │ │ │ ├── OkDialog.qml │ │ │ ├── OkDialogOverlay.qml │ │ │ ├── PasswordInput.qml │ │ │ ├── RichTextWithLinks.qml │ │ │ ├── SaveToFileButton.qml │ │ │ ├── SettingsGroupTitle.qml │ │ │ └── platformutils.js │ │ ├── connections/ │ │ │ ├── AskSecretDialog.qml │ │ │ └── ConnectionSettignsDialog.qml │ │ ├── connections-tree/ │ │ │ ├── BetterTreeView.qml │ │ │ ├── ConnectionGroupDialog.qml │ │ │ ├── TreeItemDelegate.qml │ │ │ └── menu/ │ │ │ ├── InlineMenu.qml │ │ │ ├── database.qml │ │ │ ├── key.qml │ │ │ ├── namespace.qml │ │ │ ├── server.qml │ │ │ └── server_group.qml │ │ ├── console/ │ │ │ ├── BaseConsole.qml │ │ │ ├── Consoles.qml │ │ │ └── RedisConsole.qml │ │ ├── dummy.qml │ │ ├── extension-server/ │ │ │ └── ExtensionServerSettings.qml │ │ ├── qml.qrc │ │ ├── server-actions/ │ │ │ ├── ServerAction.qml │ │ │ ├── ServerActionTabs.qml │ │ │ ├── ServerCharts.qml │ │ │ ├── ServerClients.qml │ │ │ ├── ServerConfig.qml │ │ │ ├── ServerPubSub.qml │ │ │ └── ServerSlowlog.qml │ │ ├── settings/ │ │ │ ├── BoolOption.qml │ │ │ ├── ComboboxOption.qml │ │ │ ├── FontSizeOption.qml │ │ │ ├── GlobalSettings.qml │ │ │ └── IntOption.qml │ │ └── value-editor/ │ │ ├── AddKeyDialog.qml │ │ ├── Pagination.qml │ │ ├── ValueTable.qml │ │ ├── ValueTableActions.qml │ │ ├── ValueTableCell.qml │ │ ├── ValueTabs.qml │ │ ├── editors/ │ │ │ ├── AbstractEditor.qml │ │ │ ├── HashItemEditor.qml │ │ │ ├── MultilineEditor.qml │ │ │ ├── ReadOnlySingleItemEditor.qml │ │ │ ├── SingleItemEditor.qml │ │ │ ├── SortedSetItemEditor.qml │ │ │ ├── StreamItemEditor.qml │ │ │ ├── UnsupportedDataType.qml │ │ │ ├── editor.js │ │ │ └── formatters/ │ │ │ ├── ValueFormatters.qml │ │ │ └── hexy.js │ │ └── filters/ │ │ ├── ListFilters.qml │ │ └── StreamFilters.qml │ ├── resources/ │ │ ├── Info.plist.sample │ │ ├── commands.json │ │ ├── commands.qrc │ │ ├── convert_commands.py │ │ ├── flatpak/ │ │ │ ├── app.resp.RESP.desktop │ │ │ └── app.resp.RESP.metainfo.xml │ │ ├── fonts/ │ │ │ └── OpenSans.ttc │ │ ├── fonts.qrc │ │ ├── icons.qrc │ │ ├── icons_qrc_generator.py │ │ ├── images.qrc │ │ ├── logo.icns │ │ ├── resp.desktop │ │ ├── tr.qrc │ │ └── translations/ │ │ ├── rdm.ts │ │ ├── rdm_es_ES.ts │ │ ├── rdm_ja_JP.ts │ │ ├── rdm_uk_UA.ts │ │ ├── rdm_zh_CN.ts │ │ └── rdm_zh_TW.ts │ └── resp.pro └── tests/ ├── py_tests/ │ ├── requirements.txt │ └── test_formatters/ │ ├── test_msgpack_formatter.py │ ├── test_php_formatter.py │ └── test_pickle_formatter.py ├── qml_tests/ │ ├── qml_tests.pro │ ├── setup.cpp │ ├── setup.h │ ├── tst_MultilineEditor.qml │ └── tst_formatters.qml ├── smoke_test.bat ├── tests.pro └── unit_tests/ ├── generate_coverage_report ├── main.cpp ├── respbasetestcase.h ├── testcases/ │ ├── app/ │ │ ├── app-tests.pri │ │ ├── connections.json │ │ ├── test_apputils.cpp │ │ ├── test_apputils.h │ │ ├── test_configmanager.cpp │ │ ├── test_configmanager.h │ │ ├── test_connectionsmanager.cpp │ │ ├── test_connectionsmanager.h │ │ ├── test_keymodels.cpp │ │ ├── test_keymodels.h │ │ ├── test_treeoperations.cpp │ │ └── test_treeoperations.h │ ├── connections-tree/ │ │ ├── connections-tree-tests.pri │ │ ├── mocks.cpp │ │ ├── mocks.h │ │ ├── test_databaseitem.cpp │ │ ├── test_databaseitem.h │ │ ├── test_model.cpp │ │ ├── test_model.h │ │ ├── test_serveritem.cpp │ │ └── test_serveritem.h │ ├── console/ │ │ ├── console-tests.pri │ │ ├── test_consolemodel.cpp │ │ └── test_consolemodel.h │ └── value-editor/ │ └── value-editor-tests.pri └── unit_tests.pro ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** - OS & version: [e.g. Windows 10 1806] - Redis-Server version [e.g. 5.0.1] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. ================================================ FILE: .github/workflows/run_tests.yml ================================================ name: Run Tests on: push: branches: - 2022 pull_request: branches: [ 2022 ] workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v2.3.4 with: submodules: 'recursive' - name: Install Qt uses: jurplel/install-qt-action@v2.13.2 with: version: 5.15.2 modules: qtcharts - name: Install Build deps run: | sudo apt-get update -y sudo apt-get install cmake liblz4-dev libzstd-dev libbrotli-dev libsnappy-dev lcov -y cmake --version gcc --version - name: Setup Redis uses: zhulik/redis-action@1.1.0 - name: Build Tests run: qmake "SYSTEM_LZ4=1" "SYSTEM_ZSTD=1" "SYSTEM_SNAPPY=1" "SYSTEM_BROTLI=1" DEFINES+=INTEGRATION_TESTS && make -j 2 working-directory: ./tests - name: Run Cpp Tests run: ./../bin/tests/tests -platform minimal -txt working-directory: ./tests - name: Run QML Tests run: ./../bin/tests/qml_tests -platform minimal -txt working-directory: ./tests ================================================ FILE: .github/workflows/sonar.yml ================================================ name: Sonar Scan on: push: branches: - 2022 pull_request: types: [opened, synchronize, reopened] jobs: build: name: Build runs-on: ubuntu-latest env: SONAR_SCANNER_VERSION: 4.6.1.2450 # Find the latest version in the "Linux" link on this page: # https://sonarcloud.io/documentation/analysis/scan/sonarscanner/ SONAR_SERVER_URL: "https://sonarcloud.io" BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed steps: - uses: actions/checkout@v2 with: fetch-depth: 0 submodules: 'recursive' - name: Install Qt uses: jurplel/install-qt-action@v2.13.2 with: version: 5.15.2 modules: qtcharts - name: Install system deps run: | sudo apt-get update -y sudo apt-get install cmake liblz4-dev libzstd-dev libbrotli-dev libsnappy-dev -y cmake --version gcc --version - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 - name: Cache SonarCloud packages uses: actions/cache@v1 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Download and set up sonar-scanner env: SONAR_SCANNER_DOWNLOAD_URL: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${{ env.SONAR_SCANNER_VERSION }}-linux.zip run: | mkdir -p $HOME/.sonar curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }} unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/ echo "$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin" >> $GITHUB_PATH - name: Download and set up build-wrapper env: BUILD_WRAPPER_DOWNLOAD_URL: ${{ env.SONAR_SERVER_URL }}/static/cpp/build-wrapper-linux-x86.zip run: | curl -sSLo $HOME/.sonar/build-wrapper-linux-x86.zip ${{ env.BUILD_WRAPPER_DOWNLOAD_URL }} unzip -o $HOME/.sonar/build-wrapper-linux-x86.zip -d $HOME/.sonar/ echo "$HOME/.sonar/build-wrapper-linux-x86" >> $GITHUB_PATH - name: Run build-wrapper working-directory: ./src run: | qmake "SYSTEM_LZ4=1" "SYSTEM_ZSTD=1" "SYSTEM_SNAPPY=1" "SYSTEM_BROTLI=1" build-wrapper-linux-x86-64 --out-dir ../${{ env.BUILD_WRAPPER_OUT_DIR }} make -j2 - name: Run sonar-scanner env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | sonar-scanner --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" ================================================ FILE: .gitignore ================================================ *.user *.aps *.pch *.vspscc *_i.c *_p.c *.ncb *.suo *.bak *.cache *.ilk *.log [Bb]in [Dd]ebug*/ *.sbr obj/ [Rr]elease*/ _ReSharper*/ *.sdf *.opensdf */GeneratedFiles/* build-redis* .vagrant/* redis-desktop-manager/Makefile* build/redis* build/gbreakpad* redis-desktop-manager/connections.xml *.deb deps/libssh/example/.deps/* deps/libssh/example/.* deps/libssh/example/* deps/libssh/config.status deps/libssh/docs/Makefile deps/libssh/libssh2.pc deps/libssh/libtool deps/libssh/Makefile deps/libssh/src/.* deps/libssh/*.lo deps/libssh/src/**.o deps/libssh/*.lo deps/libssh/*/*.*o deps/libssh/*/*.la deps/libssh/tests/* deps/libssh/src/libssh2_config.h deps/libssh/src/Makefile deps/libssh/src/stamp-h1 build-tests-*/* tests/Makefile tests/qml_tests/target_wrapper.sh deps/jsoncpp/buildscons/* deps/jsoncpp/dist/* deps/jsoncpp/libs/* redis-desktop-manager*.gz build/cpp-coveralls/* build/requests/* .svn/ deps/gyp* *.vsp *.psess build/windows/installer/redis-desktop-manager*.exe vagrant-provision/automake* RDM.sln.metaproj* crashreports/* redis-desktop-manager/ui_*.h src/ui_*.h src/Makefile* build-rdm-* src/rdm.pro.* tests/tests.pro.* .idea* *~ *Makefile *.DS_Store tests/unit_tests/coverage* *.qmlc *.jsc .qmake.stash parts prime stage 3rdparty/python* __pycache__/ qml_*.cpp src/modules/extension-server/server* .openapi-generator* build-* *.xcodeproj .xcode *qmlcache* ================================================ FILE: .gitmodules ================================================ [submodule "3rdparty/qredisclient"] path = 3rdparty/qredisclient url = https://github.com/uglide/qredisclient.git [submodule "3rdparty/pyotherside"] path = 3rdparty/pyotherside url = https://github.com/uglide/pyotherside.git [submodule "3rdparty/lz4"] path = 3rdparty/lz4 url = https://github.com/lz4/lz4.git [submodule "3rdparty/simdjson"] path = 3rdparty/simdjson url = https://github.com/simdjson/simdjson.git [submodule "3rdparty/zstd"] path = 3rdparty/zstd url = https://github.com/facebook/zstd.git [submodule "3rdparty/snappy"] path = 3rdparty/snappy url = https://github.com/google/snappy.git [submodule "3rdparty/brotli"] path = 3rdparty/brotli url = https://github.com/google/brotli.git [submodule "3rdparty/fakeit"] path = 3rdparty/fakeit url = https://github.com/eranpeer/FakeIt.git ================================================ FILE: .readthedocs.yaml ================================================ version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-20.04 tools: python: "3.9" mkdocs: configuration: mkdocs.yml # Optionally declare the Python requirements required to build your docs python: install: - requirements: docs/requirements.txt ================================================ FILE: 3rdparty/3rdparty.pri ================================================ #------------------------------------------------- # # Redis Desktop Manager Dependencies # #------------------------------------------------- exists( $$_PRO_FILE_PWD_/modules/extension-server/client/client.pri) { message("RESP.app Extension server integration was enabled") DEFINES += ENABLE_EXTERNAL_FORMATTERS HEADERS += $$_PRO_FILE_PWD_/modules/extension-server/dataformattermanager.h SOURCES += $$_PRO_FILE_PWD_/modules/extension-server/dataformattermanager.cpp include($$_PRO_FILE_PWD_/modules/extension-server/client/client.pri) } # qredisclient if(win32*):exists( $$PWD/qredisclient/qredisclient.lib ) { message("Using prebuilt qredisclient") INCLUDEPATH += $$PWD/qredisclient/src/ OPENSSL_LIB_PATH = C:\OpenSSL-Win64\lib\VC LIBS += -L$$OPENSSL_LIB_PATH -llibeay32MD -L$$PWD/qredisclient/ -lqredisclient -lbotan -llibssh2 -lgdi32 -lws2_32 -lkernel32 -luser32 -lshell32 -luuid -lole32 -ladvapi32 include($$PWD/qredisclient/3rdparty/asyncfuture/asyncfuture.pri) } else:unix*:exists( $$PWD/qredisclient/libqredisclient.a ) { message("Using prebuilt qredisclient") INCLUDEPATH += $$PWD/qredisclient/src/ LIBS += -L$$PWD/qredisclient/ -lqredisclient -lbotan-2 -lssh2 -lz -lssl -lcrypto include($$PWD/qredisclient/3rdparty/asyncfuture/asyncfuture.pri) } else { message("Using qredisclient source code") include($$PWD/qredisclient/qredisclient.pri) } #PyOtherSide include($$PWD/pyotherside.pri) #LZ4 LZ4DIR = $$PWD/lz4/ INCLUDEPATH += $$LZ4DIR/lib #ZSTD ZSTDDIR = $$PWD/zstd/ INCLUDEPATH += $$ZSTDDIR/lib #Snappy SNAPPYDIR = $$PWD/snappy INCLUDEPATH += $$SNAPPYDIR #Brotli BROTLIDIR = $$PWD/brotli INCLUDEPATH += $$BROTLIDIR/c/include #SIMDJSON SIMDJSONDIR = $$PWD/simdjson/singleheader INCLUDEPATH += $$SIMDJSONDIR/ HEADERS += $$SIMDJSONDIR/simdjson.h SOURCES += $$SIMDJSONDIR/simdjson.cpp win32* { ZLIBDIR = $$PWD/zlib-msvc14-x64.1.2.11.7795/build/native INCLUDEPATH += $$ZLIBDIR/include LIBS += $$ZLIBDIR/lib_release/zlibstatic.lib $$LZ4DIR/build/cmake/Release/lz4.lib LIBS += $$ZSTDDIR/build/cmake/lib/Release/zstd_static.lib LIBS += $$SNAPPYDIR/Release/snappy.lib LIBS += -L$$BROTLIDIR/Release/ -lbrotlicommon-static -lbrotlidec-static -lbrotlienc-static } unix:macx { # OSX LIBS += -lz $$LZ4DIR/build/cmake/liblz4.a $$ZSTDDIR/build/cmake/lib/libzstd.a LIBS += $$SNAPPYDIR/libsnappy.a LIBS += -L$$BROTLIDIR/ -lbrotlicommon-static -lbrotlidec-static -lbrotlienc-static } unix:!macx { # ubuntu & debian defined(CLEAN_RPATH, var) { # clean default flags message("DEB package build") QMAKE_LFLAGS_RPATH= QMAKE_LFLAGS = -Wl,-rpath=\\\$$ORIGIN/../lib QMAKE_LFLAGS += -static-libgcc -static-libstdc++ } else { # Note: uncomment if qtcreator fails to find QtCore dependencies #QMAKE_LFLAGS = -Wl,-rpath=/home/user/Qt5.9.3/5.9.3/gcc_64/lib } LIBS += -lz defined(SYSTEM_LZ4, var) { LIBS += -llz4 } else { LIBS += $$LZ4DIR/build/cmake/liblz4.a } defined(SYSTEM_ZSTD, var) { LIBS += -lzstd } else { LIBS += $$ZSTDDIR/build/cmake/lib/libzstd.a } defined(SYSTEM_SNAPPY, var) { LIBS += -lsnappy } else { LIBS += $$SNAPPYDIR/libsnappy.a } defined(SYSTEM_BROTLI, var) { LIBS += -lbrotlicommon -lbrotlidec -lbrotlienc } else { LIBS += -L$$BROTLIDIR/ -lbrotlienc-static -lbrotlicommon-static -lbrotlidec-static } # Unix signal watcher defined(LINUX_SIGNALS, var) { message("Build with qt-unix-signals") DEFINES += LINUX_SIGNALS HEADERS += $$PWD/qt-unix-signals/sigwatch.h SOURCES += $$PWD/qt-unix-signals/sigwatch.cpp INCLUDEPATH += $$PWD/qt-unix-signals/ } } ================================================ FILE: 3rdparty/pyotherside.pri ================================================ # Python PY_VERSION="39" PY_WIN_VERSION="38" PY_LIB_SUFFIX="3.9" win32* { QMAKE_LIBS += -LC:\Python$${PY_WIN_VERSION}-x64\libs -lpython$${PY_WIN_VERSION} INCLUDEPATH += C:\Python$${PY_WIN_VERSION}-x64\include\ } else { unix:macx { exists($$PWD/python-3) { message("Using Python from 3rdparty dir") LIBS += $$PWD/python-3/lib/libpython$${PY_LIB_SUFFIX}.dylib INCLUDEPATH += $$PWD/python-3/include/python$${PY_LIB_SUFFIX} #deployment PY_DATA_FILES.files = $$PWD/python-3/lib/libpython$${PY_LIB_SUFFIX}.dylib PY_DATA_FILES.path = Contents/Frameworks QMAKE_BUNDLE_DATA += PY_DATA_FILES } else { PYTHON_CONFIG = /usr/local/bin/python3-config QMAKE_LIBS += $$system($$PYTHON_CONFIG --ldflags --libs --embed) QMAKE_CXXFLAGS += $$system($$PYTHON_CONFIG --includes) } } else { PYTHON_CONFIG = python3-config PYTHON_VERSION = $$str_member($$system(python3 --version), 7, 11) message("Python version $$PYTHON_VERSION") versionAtLeast(PYTHON_VERSION, "3.8.0") { QMAKE_LIBS += $$system($$PYTHON_CONFIG --ldflags --libs --embed) } else { QMAKE_LIBS += $$system($$PYTHON_CONFIG --ldflags --libs) } QMAKE_CXXFLAGS += $$system($$PYTHON_CONFIG --includes) DEFINES *= HAVE_DLADDR } } include(pyotherside/pyotherside.pri) DEFINES += PYOTHERSIDE_VERSION=\\\"$${VERSION}\\\" DEPENDPATH += $$PWD/pyotherside/src INCLUDEPATH += $$PWD/pyotherside/src PYOTHERSIDE_DIR = $$PWD/pyotherside/src/ # Importer from Qt Resources RESOURCES += $$PYOTHERSIDE_DIR/qrc_importer.qrc HEADERS += $$PYOTHERSIDE_DIR/pythonlib_loader.h\ $$PWD/pyotherside/src/callback.h SOURCES += $$PYOTHERSIDE_DIR/pythonlib_loader.cpp # Python QML Object SOURCES += $$PYOTHERSIDE_DIR/qpython.cpp HEADERS += $$PYOTHERSIDE_DIR/qpython.h SOURCES += $$PYOTHERSIDE_DIR/qpython_worker.cpp HEADERS += $$PYOTHERSIDE_DIR/qpython_worker.h SOURCES += $$PYOTHERSIDE_DIR/qpython_priv.cpp HEADERS += $$PYOTHERSIDE_DIR/qpython_priv.h HEADERS += $$PYOTHERSIDE_DIR/python_wrap.h # Globally Load Python hack SOURCES += $$PYOTHERSIDE_DIR/global_libpython_loader.cpp HEADERS += $$PYOTHERSIDE_DIR/global_libpython_loader.h # Reference-counting PyObject wrapper class SOURCES += $$PYOTHERSIDE_DIR/pyobject_ref.cpp HEADERS += $$PYOTHERSIDE_DIR/pyobject_ref.h # QObject wrapper class exposed to Python SOURCES += $$PYOTHERSIDE_DIR/qobject_ref.cpp HEADERS += $$PYOTHERSIDE_DIR/qobject_ref.h HEADERS += $$PYOTHERSIDE_DIR/pyqobject.h # GIL helper HEADERS += $$PYOTHERSIDE_DIR/ensure_gil_state.h # Type System Conversion Logic HEADERS += $$PYOTHERSIDE_DIR/converter.h HEADERS += $$PYOTHERSIDE_DIR/qvariant_converter.h HEADERS += $$PYOTHERSIDE_DIR/pyobject_converter.h HEADERS += $$PYOTHERSIDE_DIR/qml_python_bridge.h ================================================ FILE: BACKERS.md ================================================ ## RDM Backers 1. peters 2. WillPerone 3. cblage 4. richard.hoogenboom 5. rodogu 6. markoan 7. tomlobato 8. sun.ming.77 9. Wrhector 10. trelsco 11. Sai P.S. 12. mostly-harmless 13. chasm 14. Clayton Sayer 15. henkvos 16. syrusm 17. stgogm 18. pmercier 19. elliots 20. Itamar Haber 21. Kelson 22. linux_china 23. mjirby 24. cristianobaptista 25. Scott Steele 26. caywood 27. GuRui 28. ryanski44 29. alex.mirrr 30. andrewjknox 31. chrisgo 32. Rob T. 33. chrismckee 34. ritxi 35. Recumbented 36. imesner 37. ragboy 38. tinou.bao 39. dbrugne 40. brianberlin 41. noocyte 42. yu, Wu 43. Alejandra 44. ne0zen 45. Macarun 46. Mitch 47. STRML 48. somebody 49. sachinwalia 50. Wayne Robinson 51. PyYoshi 52. JHoffmanME 53. sebastian.stanisor 54. xurumelous 55. nilskp 56. science 57. cicorias 58. BrianLocke 59. anoordende 60. pablovilas 61. runes83 62. chentex 63. forcer 64. ikary 65. eduardomcrodrigues 66. Christophe Cholot 67. mickdelaney 68. SwaroopH 69. David Jonasson 70. dean.mehmet 71. lyhdj001 72. gary.weng.10 73. okachan_0417 74. xbtequila 75. ducu 76. timeblimp 77. rduplain 78. Salada 79. djolaq 80. Alric 81. patrick 82. descipar 83. marcin.glenszczyk 84. Benni 85. ksatirli 86. devcrust 87. Soheil 88. rsafier 89. leftis 90. Brayyy 91. artsard 92. irvingswiftj 93. KeyManPL 94. atierant 95. tomascayuelas 96. kiyoaki 97. Jesper Niedermann 98. Jingjie Zheng 99. humiaozuzu 100. rolfvreijdenberger ================================================ FILE: CONTRIBUTING.md ================================================ ## IMPORTANT: HOW TO ADD ISSUES * GitHub issues **SHOULD ONLY BE USED to report bugs**, and for DETAILED feature requests. Everything else belongs to the [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/uglide/RedisDesktopManager) **PLEASE DO NOT POST GENERAL QUESTIONS** that are not about bugs or suspected bugs in the GitHub issues system. We'll be very happy to help you and provide all the support in the [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/uglide/RedisDesktopManager) ### Bug report template: Version: Environment: Redis Server Version: Steps to reproduce: 1. 2. 3. Expected result: Actual Result: ### Example of bug report: Version: 0.6.2 Environment: Windows 7 SP1 x64 Redis Server Version: 2.8.1 Steps to reproduce: 1.Click on RedisDesktopManager.ink 2.Click on Add Connection button Expected result: Active dialog window Actual Result: Crash ================================================ FILE: COPYRIGHT ================================================ RESP.app (formerly RedisDesktopManager), Cross-platform GUI management tool for Redis® Copyright 2013-2022, Ihor Malinovskyi. The RESP.app is released under the terms of the GNU General Public License, version 3. The RESP.app Project includes files written by third parties and used with permission or subject to their respective license agreements. ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ ## RESP.app - GUI for Redis ® (Formerly RedisDesktopManager) ### RESP.app is joining forces with Redis to offer the Redis community the best possible developer experience and to increase productivity when developing with Redis. Please read [this blog post](https://redis.com/blog/respapp-joining-redis/) where we share more details, and you can also visit the [FAQ](https://resp.app/faq).
![RESP.app screenshot](http://resp.app/static/img/features/all.png?v2021) ================================================ FILE: build/windows/installer/include/install_vcredist_x64.nsh ================================================ !include LogicLib.nsh !macro InstallVCredist !define VCplus_URL "https://aka.ms/vs/16/release/VC_redist.x64.exe" ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" Bld ${If} $0 >= 27033 goto VCInstalled ${Else} goto VCDownload ${EndIf} VCDownload: DetailPrint "Beginning download of VC++ 2015-2019 Redistributable." inetc::get /TIMEOUT=30000 ${VCplus_URL} "$TEMP\vc_redist.x64.exe" /END Pop $0 DetailPrint "Result: $0" StrCmp $0 "OK" InstallVCplusplus StrCmp $0 "cancelled" VCCanceled inetc::get /TIMEOUT=30000 /NOPROXY ${VCplus_URL} "$TEMP\vc_redist.x64.exe" /END Pop $0 DetailPrint "Result: $0" StrCmp $0 "OK" InstallVCplusplus MessageBox MB_ICONEXCLAMATION "Cannot download VC++ 2015-2019 Redistributable. Please install it manually if you experience any issues: ${VCplus_URL}" ExecShell open "${VCplus_URL}" goto VCInstalled InstallVCplusplus: DetailPrint "Completed download." Pop $0 ${If} $0 == "cancel" MessageBox MB_YESNO|MB_ICONEXCLAMATION \ "Download cancelled. Continue Installation?" \ IDYES VCInstalled IDNO VCCanceled ${EndIf} DetailPrint "Pausing installation while downloaded VC++ installer runs." DetailPrint "Installation could take several minutes to complete." ExecWait '$TEMP\vc_redist.x64.exe /passive /norestart' DetailPrint "Removing VC++ installer." Delete "$TEMP\vc_redist.x64.exe" DetailPrint "VC++ installer removed." goto VCInstalled VCCanceled: Abort "Installation cancelled by user." VCInstalled: Pop $0 !macroend ================================================ FILE: build/windows/installer/include/nsProcess.nsh ================================================ !define nsProcess::FindProcess `!insertmacro nsProcess::FindProcess` !macro nsProcess::FindProcess _FILE _ERR nsProcess::_FindProcess /NOUNLOAD `${_FILE}` Pop ${_ERR} !macroend !define nsProcess::KillProcess `!insertmacro nsProcess::KillProcess` !macro nsProcess::KillProcess _FILE _ERR nsProcess::_KillProcess /NOUNLOAD `${_FILE}` Pop ${_ERR} !macroend !define nsProcess::CloseProcess `!insertmacro nsProcess::CloseProcess` !macro nsProcess::CloseProcess _FILE _ERR nsProcess::_CloseProcess /NOUNLOAD `${_FILE}` Pop ${_ERR} !macroend !define nsProcess::Unload `!insertmacro nsProcess::Unload` !macro nsProcess::Unload nsProcess::_Unload !macroend ================================================ FILE: build/windows/installer/include/x64.nsh ================================================ ; --------------------- ; x64.nsh ; --------------------- ; ; A few simple macros to handle installations on x64 machines. ; ; RunningX64 checks if the installer is running on x64. ; ; ${If} ${RunningX64} ; MessageBox MB_OK "running on x64" ; ${EndIf} ; ; DisableX64FSRedirection disables file system redirection. ; EnableX64FSRedirection enables file system redirection. ; ; SetOutPath $SYSDIR ; ${DisableX64FSRedirection} ; File some.dll # extracts to C:\Windows\System32 ; ${EnableX64FSRedirection} ; File some.dll # extracts to C:\Windows\SysWOW64 ; !ifndef ___X64__NSH___ !define ___X64__NSH___ !include LogicLib.nsh !macro _RunningX64 _a _b _t _f !insertmacro _LOGICLIB_TEMP System::Call kernel32::GetCurrentProcess()i.s System::Call kernel32::IsWow64Process(is,*i.s) Pop $_LOGICLIB_TEMP !insertmacro _!= $_LOGICLIB_TEMP 0 `${_t}` `${_f}` !macroend !define RunningX64 `"" RunningX64 ""` !macro DisableX64FSRedirection System::Call kernel32::Wow64EnableWow64FsRedirection(i0) !macroend !define DisableX64FSRedirection "!insertmacro DisableX64FSRedirection" !macro EnableX64FSRedirection System::Call kernel32::Wow64EnableWow64FsRedirection(i1) !macroend !define EnableX64FSRedirection "!insertmacro EnableX64FSRedirection" !endif # !___X64__NSH___ ================================================ FILE: build/windows/installer/installer.nsi ================================================ !addincludedir .\include !addplugindir .\plugin Name "RESP.app (formerly RedisDesktopManager)" BrandingText "Open source Developer GUI for Redis" RequestExecutionLevel admin SetCompress auto SetCompressor /SOLID /FINAL lzma ManifestDPIAware true # General Symbol Definitions !define REGKEY "SOFTWARE\$(Name)" !define COMPANY "Igor Malinovskiy" !define URL resp.app !define APP_EXE "resp.exe" # MUI Symbol Definitions !define MUI_ICON "..\..\..\src\resources\images\logo.ico" !define MUI_FINISHPAGE_NOAUTOCLOSE !define MUI_FINISHPAGE_RUN $INSTDIR\${APP_EXE} !define MUI_UNICON "..\..\..\src\resources\images\logo.ico" !define MUI_WELCOMEFINISHPAGE_BITMAP ".\images\main.bmp" # Included files !include "nsProcess.nsh" !include "x64.nsh" !include "install_vcredist_x64.nsh" !include Sections.nsh !include MUI2.nsh # Variables Var StartMenuGroup # Installer pages !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_LICENSE ..\..\..\LICENSE !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES # Installer languages !insertmacro MUI_LANGUAGE English # Installer attributes OutFile resp-${VERSION}.exe InstallDir $PROGRAMFILES64\RESP_app CRCCheck on XPStyle on ShowInstDetails show VIProductVersion ${VERSION}.0 VIAddVersionKey /LANG=${LANG_ENGLISH} ProductName "RESP.app (formerly RedisDesktopManager)" VIAddVersionKey /LANG=${LANG_ENGLISH} ProductVersion "${VERSION}" VIAddVersionKey /LANG=${LANG_ENGLISH} CompanyName "${COMPANY}" VIAddVersionKey /LANG=${LANG_ENGLISH} CompanyWebsite "${URL}" VIAddVersionKey /LANG=${LANG_ENGLISH} FileVersion "${VERSION}" VIAddVersionKey /LANG=${LANG_ENGLISH} FileDescription "" VIAddVersionKey /LANG=${LANG_ENGLISH} LegalCopyright "" InstallDirRegKey HKLM "${REGKEY}" Path ShowUninstDetails show # Installer sections Section -Main SEC0000 ${nsProcess::KillProcess} "rdm.exe" $R4 ${nsProcess::KillProcess} "${APP_EXE}" $R4 ${IfNot} ${RunningX64} MessageBox MB_OK "Starting from version 2019.0.0, RESP.app doesn't support 32-bit Windows" Quit ${EndIf} IfFileExists $INSTDIR\uninstall.exe already_installed not_installed already_installed: CopyFiles /SILENT /FILESONLY "$INSTDIR\uninstall.exe" "$INSTDIR\uninstall_.exe" ExecWait '"$INSTDIR\uninstall_.exe" /S _?=$INSTDIR' Sleep 100 Delete /REBOOTOK $INSTDIR\uninstall_.exe not_installed: SetOutPath $INSTDIR File /r resources\* WriteRegStr HKLM "${REGKEY}\Components" Main 1 !insertmacro InstallVCredist BringToFront SectionEnd Section -post SEC0001 WriteRegStr HKLM "${REGKEY}" Path $INSTDIR SetOutPath $INSTDIR WriteUninstaller $INSTDIR\uninstall.exe SetOutPath $SMPROGRAMS\$StartMenuGroup CreateShortCut "$DESKTOP\RESP.lnk" "$INSTDIR\${APP_EXE}" "" IfSilent 0 +2 Exec "$INSTDIR\${APP_EXE}" CreateShortcut "$SMPROGRAMS\$StartMenuGroup\RESP.lnk" "$INSTDIR\${APP_EXE}" CreateShortcut "$SMPROGRAMS\$StartMenuGroup\$(^UninstallLink).lnk" $INSTDIR\uninstall.exe WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" DisplayName "$(^Name)" WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" DisplayVersion "${VERSION}" WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" Publisher "${COMPANY}" WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" URLInfoAbout "${URL}" WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" DisplayIcon $INSTDIR\uninstall.exe WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" UninstallString $INSTDIR\uninstall.exe WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" NoModify 1 WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" NoRepair 1 SectionEnd # Macro for selecting uninstaller sections !macro SELECT_UNSECTION SECTION_NAME UNSECTION_ID Push $R0 ReadRegStr $R0 HKLM "${REGKEY}\Components" "${SECTION_NAME}" StrCmp $R0 1 0 next${UNSECTION_ID} !insertmacro SelectSection "${UNSECTION_ID}" GoTo done${UNSECTION_ID} next${UNSECTION_ID}: !insertmacro UnselectSection "${UNSECTION_ID}" done${UNSECTION_ID}: Pop $R0 !macroend # Uninstaller sections Section /o -un.Main UNSEC0000 ${nsProcess::KillProcess} "${APP_EXE}" $R4 Sleep 1000 Delete /REBOOTOK $INSTDIR\* RmDir /REBOOTOK /r $INSTDIR\* DeleteRegValue HKLM "${REGKEY}\Components" Main SectionEnd Section -un.post UNSEC0001 DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" Delete /REBOOTOK "$DESKTOP\RESP.lnk" Delete /REBOOTOK "$SMPROGRAMS\$StartMenuGroup\RESP.lnk" Delete /REBOOTOK "$SMPROGRAMS\$StartMenuGroup\$(^UninstallLink).lnk" Delete /REBOOTOK $INSTDIR\uninstall.exe DeleteRegValue HKLM "${REGKEY}" Path DeleteRegKey /IfEmpty HKLM "${REGKEY}\Components" DeleteRegKey /IfEmpty HKLM "${REGKEY}" RmDir /REBOOTOK $SMPROGRAMS\$StartMenuGroup RmDir /REBOOTOK $INSTDIR SectionEnd # Installer functions Function .onInit InitPluginsDir StrCpy $StartMenuGroup RESP FunctionEnd # Uninstaller functions Function un.onInit SetAutoClose true ReadRegStr $INSTDIR HKLM "${REGKEY}" Path StrCpy $StartMenuGroup RESP !insertmacro SELECT_UNSECTION Main ${UNSEC0000} FunctionEnd # Installer Language Strings LangString ^UninstallLink ${LANG_ENGLISH} "Uninstall $(^Name)" ================================================ FILE: build/windows/installer/resources/qt.conf ================================================ [Paths] Prefix=.. ================================================ FILE: docs/app-store.md ================================================ ## Limitations of App Store version * AppStore version of RESP.app doesn't support [Native Formatters](native-formatters.md) ================================================ FILE: docs/bulk-operations.md ================================================ # Bulk operations *** RESP.app simplifies your Redis daily routines with bulk operations. To access bulk operations connect to Redis server and click on a target database like db0: ## Supported bulk operations ### Flush database It's a useful operation if you need to invalidate cache in a couple clicks instead of firing `FLUSHDB` command. > !!! warning "Be careful" Do not use it on Production servers. You can safeguard your Production Redis server by using [a restricted user with limited permissions](https://redis.io/docs/manual/security/acl/). ### Delete keys with filter If you need to remove some specific keys or a ["namespace"](lg-keyspaces.md#use-namespaced-keys) from your Redis server this bulk operation can come in handy. It allows you to specify a glob style pattern to define which keys should be removed. ![](http://resp.app/static/docs/bulk_delete_keys.png?v=1) ### Set TTL for multiple keys As you know, Redis is an in-memory database. You should be careful and set appropriate TTL for all keys otherwise Redis can crash or stop responding after consuming all available memory. If you realized that some keys have wrong TTL values or don't have TTL at all you can fix it using RESP.app: ![](http://resp.app/static/docs/bulk_ttl.png?v=1) ### Copy keys from one Redis server to another Sometimes you need to copy some keys from a Production Redis server to local one for debugging or vice-versa. You can achieve that by writing custom script, however it's much easier to just make a couple of clicks in RESP.app to copy keys: > !!! warning "Limitations" Currently RESP.app supports only copying data between redis-servers with the same RDB version. Usually it means that major versions of both Redis servers should be the same. ![](http://resp.app/static/docs/bulk_copy_keys.png?v=1) ### Import keys directly from RDB files Usually, production Redis servers have [AOF or RDB back-ups or persistent files](https://redis.io/docs/manual/persistence/). While AOF is basically a file with all commands that should be played again to reconstruct original dataset, RDB files don't have such flexibility. Therefore, RESP.app provides a convenient way to easily import subset of data for debugging and testing directly from RDB file instead of creating additional load to your Production system. ![](http://resp.app/static/docs/bulk_import_rdb.png?v=1) #### Is your use case not covered in RESP.app? [Contact us](mailto:support@resp.app), and we will do our best to solve it! ================================================ FILE: docs/css/extra.css ================================================ img { max-height: 500px; } code { font-size: 11pt; } ================================================ FILE: docs/development.md ================================================ ### Build RESP.app from source See [instruction](install.md#build-from-source) ### Generate test data Open RESP.app console or redis-cli and execute: ```lua eval "for index = 0,100000 do redis.call('SET', 'test_key' .. index, index) end" 0 eval "for index = 0,100000 do redis.call('SET', 'test_key:' .. math.random(1, 100) .. ':' .. math.random(1,100), index) end" 0 eval "for index = 0,100000 do redis.call('HSET', 'test_large_hash', index, index) end" 0 eval "for index = 0,100000 do redis.call('ZADD', 'test_large_zset', index, index) end" 0 eval "for index = 0,100000 do redis.call('SADD', 'test_large_set', index) end" 0 eval "for index = 0,100000 do redis.call('LPUSH', 'test_large_list', index) end" 0 ``` ### App profiling ```bash sudo apt-get install valgrind sudo add-apt-repository ppa:kubuntu-ppa/backports sudo apt-get update sudo apt-get install massif-visualizer export LD_LIBRARY_PATH="/usr/share/redis-desktop-manager/lib":$LD_LIBRARY_PATH valgrind --tool=massif --massif-out-file=rdm.massif /usr/share/redis-desktop-manager/bin/rdm ``` ### Debug SSL ```bash openssl s_client -connect HOST:PORT -cert test_user.crt -key test.key -CAfile test_ca.pem ``` ### Remove app settings on OSX ```bash rm $HOME/Library/Preferences/com.redisdesktop.RedisDesktopManager.plist killall -u `whoami` cfprefsd ``` ### Fix bugs or implement whatever you want :) ================================================ FILE: docs/extension-server.md ================================================ ## RESP.app Extension Server Developers love Redis because it gives freedom to store anything they want in it. RESP.app shares this ideology by supporting automatic decompression (GZIP, LZ4, ZSTD, BROTLI, Snappy) and deserialization of common formats like MsgPack, PHP Sessions, CBOR and Pickle. Is your serialization format not mentioned above? Continue reading to find out how to easily view your data in RESP.app. ### What is it? Starting from version `2022.4` RESP.app comes with a built-in client for Extension Server. Extension Server is a simple REST API defined by the following [OpenAPI Specification](extension-server.md#openapi-v3-specification). This server allows you to support any custom compression or serialization format. ### Build your own Extension Server in minutes Thanks to [OpenAPI Generator](https://openapi-generator.tech/docs/installation) you can generate boilerplate for your Extension Server in a couple of minutes. 1. [Install OpenAPI Generator](https://openapi-generator.tech/docs/installation) 2. Select [appropriate server generator](https://openapi-generator.tech/docs/generators#server-generators). 3. Download spec file from `https://raw.githubusercontent.com/uglide/RedisDesktopManager/2022/docs/server_spec.yaml` 4. Generate server:
` openapi-generator generate -i server_spec.yaml -g YOUR_GENERATOR -o my_extension_server ` 5. Open `my_extension_server` in your favorite IDE and start adding your custom formatters to generated server. **If you are faced with any issues you can [contact support](mailto:support@resp.app) or ask for help in [telegram chat](https://t.me/RedisDesktopManager)** ### Connect to Extension Server in RESP.app 1. Ensure that you are using RESP.app version `2022.4` or above 2. Click on the "Extension Server" button in top right corner of the main window 3. In the Extension Server dialog specify your server URL and basic auth details if any: 4. Hit Reload button ### Visualizing data with Extension Server RESP.app supports following `Content-Type` responses from Extension Server: - `application/json` - `image/*` for example `image/svg+xml` This allows you to perform any required preprocessing and visualize your data: ### OpenAPI v3 Specification **Please submit your proposals to the following spec on [GitHub](https://github.com/uglide/RedisDesktopManager/issues)** !!swagger server_spec.yaml!! ### Third-party extension servers You can find some examples on [GitHub](https://github.com/search?q=resp.app+extension+server). ================================================ FILE: docs/faq.md ================================================ ## Where is the connections config stored? **Windows** `%USERPROFILE%\.rdm\connections.json` **macOS dmg** `$HOME/Library/Preferences/rdm/connections.json` **macOS App Store** `$HOME/Library/Containers/com.redisdesktop.rdm/Data/Library/Preferences/rdm/` **Linux flatpak** `$HOME/.rdm/connections.json` **Linux snap** `$HOME/snap/redis-desktop-manager/common/.rdm/connections.json` ================================================ FILE: docs/index.md ================================================ # RESP.app Documentation RESP.app (formerly RedisDesktopManager) — is a cross-platform open source GUI for Redis ® available on Windows, Linux and macOS. This tool offers you an easy-to-use GUI to access your Redis ® DB and perform some basic operations: view keys as a tree, CRUD keys, execute commands via shell. RESP.app supports SSL/TLS encryption, SSH tunnels and cloud Redis instances, such as: Amazon ElastiCache, Microsoft Azure Redis Cache and other Redis ® clouds. Please submit any issues and proposals on [GitHub](https://github.com/uglide/RedisDesktopManager/issues) #### Ask for help in [telegram chat](https://t.me/RedisDesktopManager) ================================================ FILE: docs/install.md ================================================ # Quick Install ## Windows 1. Install [Microsoft Visual C++ 2015-2019 x64](https://aka.ms/vs/16/release/vc_redist.x64.exe) (If you have not already). 2. Download Windows Installer from [http://resp.app/subscriptions](http://resp.app/subscriptions). **(Requires subscription)** 3. Run the downloaded installer. ## Mac OS X 1. Download dmg image from [http://resp.app/subscriptions](http://resp.app/subscriptions). **(Requires subscription)** 2. Mount the DMG image. 3. Run rdm.app. ## Ubuntu / ArchLinux / Debian / Fedora / CentOS / OpenSUSE / etc ### Install flatpak 1. Install RESP.app using [Flathub](https://flathub.org/apps/details/app.resp.RESP). > !!! info "How to install in command line" Make sure to follow the [setup guide](https://flatpak.org/setup/) before installing
`flatpak install flathub app.resp.RESP` > !!! tip "How to run" If RESP.app icon hasn't appeared in your application launcher, you can run RESP.app from terminal with:
`flatpak run app.resp.RESP` ### Install snap 1. Install RESP.app using [Snapcraft](https://snapcraft.io/redis-desktop-manager). > !!! warning "SSH Keys" To be able to access your ssh keys from RESP.app please connect `ssh-key` interface: `sudo snap connect redis-desktop-manager:ssh-keys` > !!! tip "How to Run" If RESP.app icon hasn't appeared in your application launcher you can run RESP.app from terminal `/snap/bin/redis-desktop-manager.rdm` ## Build from source ### Get source 1. Install git using the instructions here: https://git-scm.com/download 2. Get the source code: ``` git clone --recursive https://github.com/uglide/RedisDesktopManager.git -b 2022 rdm && cd ./rdm ``` > !!! warning "SSH Tunneling support" Since 0.9.9 RESP.app by default does not include SSH Tunneling support. You can create a SSH tunnel to your Redis server manually and connect to `localhost`: `ssh -L 6379:REDIS_HOST:6379 SSH_USER@SSH_HOST -P SSH_PORT -i SSH_KEY -T -N` or [use pre-built binary for your OS](#quick-install) ### Build on OS X 1. Install [Xcode](https://developer.apple.com/xcode/) with Xcode build tools. 2. Install [Homebrew](http://brew.sh/). 3. Copy `cd ./src && cp ./resources/Info.plist.sample ./resources/Info.plist`. 4. Building RESP.app dependencies require i.a. `openssl`, `cmake` and `python3`. Install them: `brew install openssl cmake python3` 5. Build lz4 lib ``` cd 3rdparty/lz4/build/cmake cmake -DLZ4_BUNDLED_MODE=ON . make cd 3rdparty/brotli cmake -DBUILD_SHARED_LIBS=OFF make cd 3rdparty/snappy cmake -DHAVE_LIBLZO2=0 -DHAVE_LIBLZ4=0 && make cd 3rdparty/zstd/build/cmake cmake ./ && make ``` 6. Install Python requirements `pip3 install -t ../bin/osx/release -r py/requirements.txt` 7. Install [Qt 5.15](http://www.qt.io/download-open-source/#section-2). Add Qt Creator and under Qt 5.15.x add Qt Charts module. 8. Open `./src/rdm.pro` in **Qt Creator**. 9. Run build. ### Build on Windows 1. Install [Visual Studio 2019 Community Edition](https://visualstudio.microsoft.com/vs/). 2. Install [Qt 5.15](https://www.qt.io/download). 3. Go to `3rdparty/qredisclient/3rdparty/hiredis` and apply the patch to fix compilation on Windows: `git apply ../hiredis-win.patch` 4. Go to the `3rdparty/` folder and install zlib with `nuget`: `nuget install zlib-msvc14-x64 -Version 1.2.11.7795` 5. Build lz4 lib ``` cd 3rdparty/lz4/build/cmake cmake -DLZ4_BUNDLED_MODE=ON . make ``` 6. Install Python 3.9 amd64 to `C:\Python39-x64`. 7. Install Python requirements `pip3 install -r src/py/requirements.txt`. 8. Open `./src/rdm.pro` in **Qt Creator**. Choose the `Desktop Qt 5.15.x MSVC2019 64bit > Release` build profile. 9. Run build. (Just hit `Ctrl-B`) ================================================ FILE: docs/known-issues.md ================================================ ### Application looks corrupted on my 1080p screen on Linux (too small font and/or broken dialogs) Run RESP.app from terminal without Qt Autoscaling: `Exec=env QT_AUTO_SCREEN_SCALE_FACTOR=0 redis-desktop-manager.rdm` ================================================ FILE: docs/lg-keyspaces.md ================================================ # Working with large keyspaces By default, RESP.app uses `*` (wildcard glob-style pattern) in `SCAN` command to load all keys from the selected database. It’s simple and user-friendly for cases when you have only a couple of thousands keys. But for production redis-servers with millions of keys it leads to a huge amount of time needed to load keys in RESP.app. On this page you will find different approaches how to work with large Redis keyspaces efficiently. ## Increase limit for `SCAN` command RESP.app limits amount of keys that should be scanned by Redis to `10000`. If you have more than 100K keys in Redis it's recommended to increase this limit to `50000` or `100000`. > !!! warning "Be careful!" High scanning limit may affect your Redis performance! To increase this limit click on the Settings button in top right corner for the main window and change value for `Limit for SCAN command` setting. ## Use specific `SCAN` filter to reduce loaded amount of keys Consider using more specific filters for `SCAN` in order to speed up keys loading and reduce memory footprint 1. Right click on database and click on Filter button
2. Enter glob-style pattern and press apply button
> !!! note More details about `SCAN` filter syntax you can find in Redis documentation [https://redis.io/commands/scan#the-match-option]() Default `SCAN` filter can be changed in connection settings on “Advanced Settings” tab:
## Use namespaced keys Colon sign `:` is a commonly used convention when naming Redis keys. For example you can use following schema to store information about users: `user:1000` Following this schema allows you to simplify removal of obsolete keys and performing other operations with keys in Redis. Using namespaced keys is also important for loading huge keyspaces in RESP.app. It renders namespaces on demand (since 2020.2+) and this approach allows to visualise millions of keys with small memory footprint. Default namespace separator can be changed in connection settings on “Advanced Settings” tab. More tips about Redis keys naming you can find in this tutorial [https://redis.io/topics/data-types-intro#redis-keys]() ================================================ FILE: docs/native-formatters.md ================================================ ## Native value formatters > !!! warning "End of life" This feature was deprecated and removed from RESP.app. Please use [Extension Server](extension-server.md) instead ================================================ FILE: docs/quick-start.md ================================================ # **How to start using RESP.app** *** After you've [installed](install.md) RESP.app, the first thing you need to do in order to get going is to create a connection to your Redis server. On the main window, press the button labelled **Connect to Redis Server**. ![](http://resp.app/static/docs/rdm_main.png?v=2) ## Connect to a local or public redis-server On the first tab (Connection Settings), put in general information regarding the connection that you are creating. * **Name** - the name of new connection (example: my_local_redis) * **Host** - redis-server host (example: localhost) * **Port** - redis-server port (example: 6379) * **Password** - redis-server authentication password (if any) ([http://redis.io/commands/AUTH](http://redis.io/commands/AUTH)) * **Username** - only for redis-servers >= 6.0 with configured [ACL](https://redis.io/topics/acl), for older redis-server leave empty ## Connect to a public redis-server with SSL If you want to connect to a redis-server instance with SSL you need to enable SSL on the second tab and provide a public key in PEM format. Instructions for certain cloud services are below: ### AWS ElastiCache AWS ElastiCache is not accessible outside of your VPC. In order to connect to your ElastiCache remotely, you need to use one of the following options: * Setup VPN connection **[Recommended]** [https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/accessing-elasticache.html#access-from-outside-aws](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/accessing-elasticache.html#access-from-outside-aws) * Setup SSH proxying host and connect through SSH tunnel. **[Slow network performance. Not recommended]** * Setup NAT instance for exposing your AWS ElastiCache to the Internet **[Firewall rules should be configured very carefully. Not recommended.]** #### How to connect to AWS ElastiCache with In-Transit Encryption ##### VPN / NAT Enable SSL/TLS checkbox and connect to your AWS ElastiCache with In-Transit Encryption. ##### SSH tunnel Click on "Enable TLS-over-SSH" checkbox in the the SSH connection settings and connect to your AWS ElastiCache with In-Transit Encryption. ### Microsoft Azure Redis Cache
1. Create a connection with all requested information.
2. Make sure that the "Use SSL Protocol" checkbox is enabled. 3. Your Azure Redis connection is ready to use. ### Redis Labs
To connect to a Redis Labs instance with SSL/TLS encryption, follow the steps below: 1. Make sure that SSL is enabled for your Redis instance in the Redis Labs dashboard. 2. Download and unzip `garantia_credentials.zip` from the Redis Labs dashboard. 3. Select `garantia_user.crt` in the "Public key" field. 4. Select `garantia_user_private.key` in the "Private key" field. 5. Select `garantia_ca.pem` in the "Authority" field. ### Digital Ocean Managed Redis
Digital Ocean connection settings is a bit confusing. To connect to a Digital Ocean Managed Redis you need to follow steps bellow: 1. Copy host, port and password information to RESP.app 2. **Leave Username field in RESP.app empty** (Important!) 3. Enable SSL/TLS checkbox Or using Quick Connect tab for new connections: 1. Copy connection string (starts with "rediss://") from connection details to RESP.app 2. Click "Import" and "Test Connection" ### Heroku Redis
1. Get Redis connection string from terminal with command ``` heroku config -a YOUR-APP-NAME | grep REDIS ``` or get it from Heroku website. Example output: ``` rediss://user:password@host:port ``` 2. Enter connection settings in RESP.app Connection dialog: - If URL starts with `rediss` enable SSL/TLS checkbox and **uncheck** "Enable strict mode" checkbox - Copy `user` to "Username" field - Copy `password` to "Password" field - Copy `host` and `port` to "Address" field in RESP.app ## Connect to private redis-server via SSH tunnel ### Basic SSH tunneling SSH tab is supposed to allow you to use a SSH tunnel. It's useful if your redis-server is not publicly accessible. To use a SSH tunnel select checkbox "SSH Tunnel". There are different security options; you can use a plain password or OpenSSH private key. >!!! note "for Windows users:" Your private key must be in .pem format. ### SSH Agent Starting from version 2022.3 RESP.app supports SSH Agents. This allows using password managers like [1Password](https://developer.1password.com/docs/ssh/agent) to securely store your SSH keys with 2FA. >!!! note "for Windows users:" On Windows RESP.app supports only [Microsoft OpenSSH](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_overview) that's why "Custom SSH Agent Path" option is not available. ##### How to connect to 1Password SSH-Agent from DMG version of RESP.app It's possible to set default SSH Agent for all connections in RESP.app by overriding environment variable `SSH_AUTH_SOCK`. If you need to use custom ssh agent only for specific connections follow steps above: 1. Create symlink to agent.sock ``` mkdir -p ~/.1password && ln -s ~/Library/Group\ Containers/2BUA8C4S2C.com.1password/t/agent.sock ~/.1password/agent.sock ``` 2. In RESP.app check "Use SSH Agent" checkbox and click on the "Select File" button next to "Custom SSH Agent Path" field 3. Press `⌘ + Shift + .` to show hidden files and folders in the dialog 4. Select file `~/.1password/agent.sock` 5. Save connection settings ##### How to connect to SSH-Agent from AppStore version of RESP.app Due to AppStore sandboxing RESP.app cannot access default or custom SSH Agents defined by `SSH_AUTH_SOCK` variable. To overcome this limitation you need to create proxy unix socket inside RESP.app sandbox container: 1. Install socat with homebrew ``` brew install socat ``` 2. Create proxy unix-socket with socat: ``` socat UNIX-LISTEN:$HOME/Library/Containers/com.redisdesktop.rdm/Data/agent.sock UNIX-CONNECT:"$HOME/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" ``` ### Advanced SSH tunneling If you need advanced SSH tunneling you should setup a SSH tunnel manually and connect via localhost: ``` ssh SSH_HOST -L 7000:localhost:6379 ``` ## Connect to a UNIX socket RESP.app [doesn't support UNIX sockets](https://github.com/uglide/RedisDesktopManager/issues/1751) directly, but you can use redirecting of the local socket to the UNIX domain socket, for instance with [socat](https://sourceforge.net/projects/socat): ``` socat -v tcp-l:6379,reuseaddr,fork unix:/tmp/redis.sock ``` ## Advanced connection settings The **Advanced settings** tab allows you to customise the namespace separator and other advanced settings. ## Next steps Now you can test a connection or create a connection right away. Congratulations, you've successfully connected to your Redis Server. You should see something similar to what we show below. ![](http://resp.app/static/docs/rdm_main2.png?v=2) Click on the connection and expand keys. By clicking the right button, you can see console menu and manage your connection from there. ================================================ FILE: docs/requirements.txt ================================================ mkdocs==1.3.0 mkdocs-render-swagger-plugin==0.0.3 ================================================ FILE: docs/server_spec.yaml ================================================ openapi: 3.0.0 info: version: 2022.0-preview1 title: RESP.app Extension server description: RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters paths: /data-formatters: get: description: Returns a list of data formatters responses: '200': description: Successful response content: application/json: schema: $ref: "#/components/schemas/DataFormatters" /data-formatters/{id}/decode: post: parameters: - name: id in: path required: true description: The id of data formatter schema: type: string requestBody: content: 'application/json': schema: $ref: '#/components/schemas/DecodePayload' responses: '200': description: Successful response with correct content type. RESP.app supports text/plain, application/json and application/octet-stream content: '*/*' : schema: type: string '400': description: Validation error response content: 'application/json': schema: type: object properties: error: type: string /data-formatters/{id}/encode: post: parameters: - name: id in: path required: true description: The id of data formatter schema: type: string requestBody: content: 'application/json': schema: $ref: '#/components/schemas/EncodePayload' responses: '200': description: Successful response with content type application/octet-stream content: '*/*' : schema: type: string '400': description: Validation error response content: 'application/json': schema: type: object properties: error: type: string components: securitySchemes: basic: type: http scheme: basic schemas: DataFormatter: type: object required: - id - name properties: id: type: string description: Internal formatter ID used in requests to this API example: "1" name: type: string description: Name displayed inside RDM app example: "My .net models decoder" read-only: type: boolean description: Read-only formatters only receive decode requests DataFormatters: type: array items: $ref: "#/components/schemas/DataFormatter" DecodePayload: type: object properties: data: type: string description: Base64 encoded string redis-key-name: type: string redis-key-type: type: string EncodePayload: type: object properties: data: type: string description: Base64 encoded string metadata: type: object description: Metadata from formatter custom ui forms security: - basic: [] ================================================ FILE: mkdocs.yml ================================================ site_name: "RESP.app" site_description: "RESP.app Documentation (formerly RedisDesktopManager)" site_author: "Igor Malinovskiy" site_favicon: "favicon.ico" repo_url: https://github.com/uglide/RedisDesktopManager edit_uri: edit/2022/docs/ theme: readthedocs extra_css: - css/extra.css nav: - Home: 'index.md' - Install: 'install.md' - Quick Start: 'quick-start.md' - Bulk operations: 'bulk-operations.md' - Working with large keyspaces: 'lg-keyspaces.md' - Native Formatters: 'native-formatters.md' - Extension server: 'extension-server.md' - FAQ: 'faq.md' - Known Issues: 'known-issues.md' - AppStore Limitations: 'app-store.md' - Development Guide: 'development.md' markdown_extensions: - markdown.extensions.admonition plugins: - render_swagger ================================================ FILE: sonar-project.properties ================================================ sonar.projectName=RedisDesktopManager sonar.projectKey=uglide_RedisDesktopManager sonar.organization=uglide sonar.projectVersion=2021.10 # SQ standard properties sonar.sources=src sonar.sourceEncoding=UTF-8 ================================================ FILE: src/app/app.cpp ================================================ #include "app.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(Q_OS_WINDOWS) || defined(Q_OS_LINUX) #include "darkmode.h" #include #endif #include "common/tabviewmodel.h" #include "events.h" #include "models/configmanager.h" #include "models/connectionconf.h" #include "models/connectionsmanager.h" #include "models/key-models/keyfactory.h" #include "modules/bulk-operations/bulkoperationsmanager.h" #include "modules/common/sortfilterproxymodel.h" #include "modules/console/autocompletemodel.h" #include "modules/console/consolemodel.h" #include "modules/server-actions/serverstatsmodel.h" #include "modules/value-editor/embeddedformattersmanager.h" #ifdef ENABLE_EXTERNAL_FORMATTERS #include "modules/extension-server/dataformattermanager.h" #endif #include "modules/value-editor/syntaxhighlighter.h" #include "modules/value-editor/textcharformat.h" #include "modules/value-editor/tabsmodel.h" #include "modules/value-editor/valueviewmodel.h" #include "qmlutils.h" #ifdef Q_OS_WINDOWS #include #endif Application::Application(int& argc, char** argv) : QApplication(argc, argv), m_engine(this), m_qmlUtils(QSharedPointer(new QmlUtils())), m_events(QSharedPointer(new Events())) { // Init components required for models and qml initAppInfo(); initProxySettings(); processCmdArgs(); initAppFonts(); #if defined(Q_OS_WINDOWS) || defined(Q_OS_LINUX) if (isDarkThemeEnabled()) { setStyle(QStyleFactory::create("Fusion")); setPalette(createDarkModePalette()); } #endif initRedisClient(); installTranslator(); } void Application::initModels() { ConfigManager confManager(m_settingsDir); QString config = confManager.getApplicationConfigPath("connections.json"); if (config.isNull()) { QMessageBox::critical( nullptr, QCoreApplication::translate("RESP", "Settings directory is not writable"), QCoreApplication::translate( "RESP", "RESP.app can't save connections file to settings directory. " "Please change file permissions or restart RESP.app as " "administrator.")); throw std::runtime_error("invalid connections config"); } m_keyFactory = QSharedPointer(new KeyFactory()); m_keyValues = QSharedPointer(new ValueEditor::TabsModel( m_keyFactory.staticCast(), m_events)); connect(m_events.data(), &Events::openValueTab, m_keyValues.data(), &ValueEditor::TabsModel::openTab); connect(m_events.data(), &Events::newKeyDialog, m_keyFactory.data(), &KeyFactory::createNewKeyRequest); connect(m_events.data(), &Events::closeDbKeys, m_keyValues.data(), &ValueEditor::TabsModel::closeDbKeys); m_connections = QSharedPointer( new ConnectionsManager(config, m_events)); m_bulkOperations = QSharedPointer( new BulkOperations::Manager(m_connections)); connect(m_events.data(), &Events::requestBulkOperation, m_bulkOperations.data(), &BulkOperations::Manager::requestBulkOperation); m_consoleModel = QSharedPointer( new TabViewModel(getTabModelFactory())); connect(m_events.data(), &Events::openConsole, m_consoleModel.data(), &TabViewModel::openTab); auto srvStatsFactory = [this](QSharedPointer c, int dbIndex, QList initCmd) { auto model = QSharedPointer( new ServerStats::Model(c, dbIndex, initCmd), &QObject::deleteLater); QObject::connect(model.staticCast().data(), &ServerStats::Model::openConsoleTerminal, m_events.data(), &Events::openConsole); return model; }; m_serverStatsModel = QSharedPointer( new TabViewModel(srvStatsFactory)); connect(m_events.data(), &Events::openServerStats, this, [this](QSharedPointer c) { m_serverStatsModel->openTab(c, 0, false); }); #ifdef ENABLE_EXTERNAL_FORMATTERS m_extServerManager = QSharedPointer(new RespExtServer::DataFormattersManager(m_engine)); connect(m_extServerManager.data(), &RespExtServer::DataFormattersManager::error, this, [this](const QString& msg) { qDebug() << "External formatters:" << msg; m_events->log(QString("External: %1").arg(msg)); }); connect(m_extServerManager.data(), &RespExtServer::DataFormattersManager::loaded, this, [this]() { qDebug() << "External formatters loaded"; emit m_events->externalFormattersLoaded(); }); if (!m_extServerUrl.isEmpty()) { m_extServerManager->setUrl(m_extServerUrl); } connect(m_events.data(), &Events::appRendered, this, [this]() { if (m_extServerManager) m_extServerManager->loadFormatters(); }); #endif m_embeddedFormatters = QSharedPointer( new ValueEditor::EmbeddedFormattersManager()); connect(m_embeddedFormatters.data(), &ValueEditor::EmbeddedFormattersManager::error, this, [this](const QString& msg) { m_events->log(QString("Formatters: %1").arg(msg)); }); m_consoleAutocompleteModel = QSharedPointer( new Console::AutocompleteModel()); connect(m_events.data(), &Events::appRendered, this, [this]() { if (m_connections) m_connections->loadConnections(); initPython(); if (m_embeddedFormatters) m_embeddedFormatters->init(m_python); if (m_bulkOperations) m_bulkOperations->setPython(m_python); if (m_events) emit m_events->pythonLoaded(); }); } void Application::initAppInfo() { setApplicationName("RESP.app - Developer GUI for Redis"); setApplicationVersion(QString(APP_VERSION)); setOrganizationDomain("redisdesktop.com"); setOrganizationName("redisdesktop"); #ifdef Q_OS_MAC setWindowIcon(QIcon(":/images/logo.icns")); #else setWindowIcon(QIcon(":/images/logo.png")); #endif qDebug() << "TLS support:" << QSslSocket::sslLibraryVersionString(); } void Application::initAppFonts() { QSettings settings; const int minFontSize = 4; #ifdef Q_OS_MAC QString defaultFontName("Helvetica Neue"); QString defaultMonospacedFont("Monaco"); int defaultFontSize = 12; #elif defined(Q_OS_WINDOWS) QString defaultFontName("Segoe UI"); QString defaultMonospacedFont("Consolas"); int defaultFontSize = 11; #else QString defaultFontName("Open Sans"); QString defaultMonospacedFont("Ubuntu Mono"); int defaultFontSize = 11; #endif int defaultValueSizeLimit = 150000; QString appFont = settings.value("app/appFont", defaultFontName).toString(); if (appFont.isEmpty()) appFont = defaultFontName; int appFontSize = settings.value("app/appFontSize", defaultFontSize).toInt(); if (appFontSize < minFontSize) appFontSize = defaultFontSize; if (appFont == "Open Sans") { #if defined(Q_OS_LINUX) int result = QFontDatabase::addApplicationFont("://fonts/OpenSans.ttc"); if (result == -1) { appFont = "Ubuntu"; } #elif defined (Q_OS_WINDOWS) appFont = defaultFontName; #endif } QString valuesFont = settings.value("app/valueEditorFont", defaultMonospacedFont).toString(); if (valuesFont.isEmpty()) valuesFont = defaultMonospacedFont; int valuesFontSize = settings.value("app/valueEditorFontSize", defaultFontSize).toInt(); if (valuesFontSize < minFontSize) valuesFontSize = defaultFontSize; int valueSizeLimit = settings.value("app/valueSizeLimit", defaultValueSizeLimit).toInt(); if (valueSizeLimit < 1000) valueSizeLimit = defaultValueSizeLimit; settings.setValue("app/appFont", appFont); settings.setValue("app/appFontSize", appFontSize); settings.setValue("app/valueEditorFont", valuesFont); settings.setValue("app/valueEditorFontSize", valuesFontSize); settings.setValue("app/valueSizeLimit", valueSizeLimit); qDebug() << "App font:" << appFont << appFontSize; qDebug() << "Values font:" << valuesFont; QFont defaultFont(appFont, appFontSize); QApplication::setFont(defaultFont); } void Application::initProxySettings() { QSettings settings; QNetworkProxyFactory::setUseSystemConfiguration( settings.value("app/useSystemProxy", false).toBool()); } void Application::registerQmlTypes() { qmlRegisterType("rdm.models", 1, 0, "SortFilterProxyModel"); qmlRegisterType("rdm.models", 1, 0, "SyntaxHighlighter"); qmlRegisterType("rdm.models", 1, 0, "TextCharFormat"); qRegisterMetaType(); } void Application::registerQmlRootObjects() { m_engine.rootContext()->setContextProperty("appEvents", m_events.data()); m_engine.rootContext()->setContextProperty("qmlUtils", m_qmlUtils.data()); m_engine.rootContext()->setContextProperty("connectionsManager", m_connections.data()); m_engine.rootContext()->setContextProperty("keyFactory", m_keyFactory.data()); m_engine.rootContext()->setContextProperty("valuesModel", m_keyValues.data()); #ifdef ENABLE_EXTERNAL_FORMATTERS m_engine.rootContext()->setContextProperty("formattersManager", m_extServerManager.data()); #endif m_engine.rootContext()->setContextProperty("embeddedFormattersManager", m_embeddedFormatters.data()); m_engine.rootContext()->setContextProperty("consoleModel", m_consoleModel.data()); m_engine.rootContext()->setContextProperty("serverStatsModel", m_serverStatsModel.data()); m_engine.rootContext()->setContextProperty("bulkOperations", m_bulkOperations.data()); m_engine.rootContext()->setContextProperty("consoleAutocompleteModel", m_consoleAutocompleteModel.data()); } void Application::initQml() { if (m_renderingBackend == "auto") { QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software); } else { QQuickWindow::setSceneGraphBackend(m_renderingBackend); } registerQmlTypes(); registerQmlRootObjects(); try { m_engine.load(QUrl(QStringLiteral("qrc:///app.qml"))); } catch (...) { qDebug() << "Failed to load app window. Retrying with software renderer..."; QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software); m_engine.load(QUrl(QStringLiteral("qrc:///app.qml"))); } updatePalette(); connect(this, &QGuiApplication::paletteChanged, this, &Application::updatePalette); qDebug() << "Rendering backend:" << QQuickWindow::sceneGraphBackend(); emit m_events->appRendered(); } void Application::initPython() { m_python = QSharedPointer(new QPython(this, 1, 5)); m_python->addImportPath("qrc:/python/"); #ifdef Q_OS_MACOS m_python->addImportPath(applicationDirPath() + "/../Resources/py"); #else m_python->addImportPath(applicationDirPath()); #endif } void Application::installTranslator() { QSettings settings; QString preferredLocale = settings.value("app/locale", "system").toString(); QString locale; if (preferredLocale == "system") { settings.setValue("app/locale", "system"); locale = QLocale::system().uiLanguages().first().replace("-", "_"); qDebug() << QLocale::system().uiLanguages(); if (locale.isEmpty() || locale == "C") locale = "en_US"; qDebug() << "Detected locale:" << locale; } else { locale = preferredLocale; } m_translator = QSharedPointer(new QTranslator((QObject*)this)); if (m_translator->load(QString(":/translations/rdm_") + locale)) { qDebug() << "Load translations file for locale:" << locale; QCoreApplication::installTranslator(m_translator.data()); } else { m_translator.clear(); } } void Application::processCmdArgs() { QCommandLineParser parser; QCommandLineOption settingsDir("settings-dir", "(Optional) Directory where RESP.app looks/saves " ".rdm directory with connections.json file", "settingsDir", QDir::homePath()); QCommandLineOption extensionServerUrl( "extension-server-url", "(Optional) Overrides extension server url", "extensionServerUrl", QString()); QCommandLineOption renderingBackend( "rendering-backend", "(Optional) QML rendering backend [software|opengl|d3d12|'']", "renderingBackend", "auto"); parser.addHelpOption(); parser.addVersionOption(); parser.addOption(settingsDir); parser.addOption(extensionServerUrl); parser.addOption(renderingBackend); parser.process(*this); m_settingsDir = parser.value(settingsDir); m_extServerUrl = parser.value(extensionServerUrl); m_renderingBackend = parser.value(renderingBackend); } void Application::updatePalette() { if (m_engine.rootObjects().size() == 0) { qWarning() << "Cannot update palette. Root object is not loaded."; return; } auto rootObject = m_engine.rootObjects().at(0); rootObject->setProperty("palette", QGuiApplication::palette()); #ifdef Q_OS_WINDOWS if (!isDarkThemeEnabled()) return; auto window = qobject_cast(rootObject); if (window) { auto winHwnd = reinterpret_cast(window->winId()); BOOL USE_DARK_MODE = true; BOOL SET_IMMERSIVE_DARK_MODE_SUCCESS = SUCCEEDED(DwmSetWindowAttribute( winHwnd, 20, &USE_DARK_MODE, sizeof(USE_DARK_MODE))); if (SET_IMMERSIVE_DARK_MODE_SUCCESS) { // Dirty hack to re-draw window and apply darkmode color rootObject->setProperty("visible", false); rootObject->setProperty("visible", true); } } #endif } ================================================ FILE: src/app/app.h ================================================ #pragma once #include #include #include #include #include #ifndef APP_VERSION #include "../version.h" #endif class QmlUtils; class Events; class ConnectionsManager; class Updater; class KeyFactory; class TabViewModel; class QPython; namespace ValueEditor { class TabsModel; } #ifdef ENABLE_EXTERNAL_FORMATTERS namespace RespExtServer { class DataFormattersManager; } #endif namespace ValueEditor { class EmbeddedFormattersManager; } // namespace ValueEditor namespace BulkOperations { class Manager; } namespace Console { class AutocompleteModel; } class Application : public QApplication { Q_OBJECT public: Application(int &argc, char **argv); void initModels(); void initQml(); private: void initAppInfo(); void initAppFonts(); void initProxySettings(); void initPython(); void registerQmlTypes(); void registerQmlRootObjects(); void installTranslator(); void processCmdArgs(); private slots: void updatePalette(); private: QQmlApplicationEngine m_engine; QSharedPointer m_qmlUtils; QSharedPointer m_events; QSharedPointer m_connections; QSharedPointer m_keyFactory; QSharedPointer m_keyValues; #ifdef ENABLE_EXTERNAL_FORMATTERS QSharedPointer m_extServerManager; #endif QSharedPointer m_embeddedFormatters; QSharedPointer m_bulkOperations; QSharedPointer m_consoleModel; QSharedPointer m_serverStatsModel; QSharedPointer m_consoleAutocompleteModel; QSharedPointer m_python; QString m_settingsDir; QString m_extServerUrl; QString m_renderingBackend; QSharedPointer m_translator = nullptr; }; ================================================ FILE: src/app/apputils.h ================================================ #pragma once #include #include inline QString humanReadableSize(qint64 size) { return QLocale().formattedDataSize(size, 2, QLocale::DataSizeSIFormat); } ================================================ FILE: src/app/darkmode.h ================================================ #pragma once #include #include bool isDarkThemeEnabled() { #if defined(Q_OS_WINDOWS) QSettings settings; QSettings systemSettings( "HKEY_CURRENT_" "USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", QSettings::NativeFormat); QString darkMode = settings.value("app/darkMode", "Auto").toString(); if (darkMode == "Auto") { return systemSettings.value("AppsUseLightTheme") == 0; } else if (darkMode == "On") { return true; } else { return false; } #elif defined(Q_OS_LINUX) QSettings settings; return settings.value("app/darkModeOn", false).toBool(); #else return false; #endif } QPalette createDarkModePalette() { QColor base = QColor(30, 30, 30); QColor alt = QColor(50, 50, 50); QColor text = QColor(223, 223, 223); QColor buttonText = QColor(170, 170, 170); QColor disabledColor = QColor(127, 127, 127); QPalette p(alt, base); p.setColor(QPalette::Light, QColor(76,76,76)); p.setColor(QPalette::Dark, QColor(235,235,235)); p.setColor(QPalette::Midlight, QColor(76,76,76)); p.setColor(QPalette::Mid, QColor(66,66,66)); p.setColor(QPalette::Window, base); p.setColor(QPalette::WindowText, text); p.setColor(QPalette::Base, base); p.setColor(QPalette::AlternateBase, alt); p.setColor(QPalette::ToolTipBase, alt); p.setColor(QPalette::ToolTipText, Qt::white); p.setColor(QPalette::Text, text); p.setColor(QPalette::Disabled, QPalette::Text, disabledColor); p.setColor(QPalette::Button, alt); p.setColor(QPalette::ButtonText, buttonText); p.setColor(QPalette::Disabled, QPalette::ButtonText, disabledColor); p.setColor(QPalette::BrightText, text.lighter(80)); p.setColor(QPalette::Link, QColor(42, 130, 218)); p.setColor(QPalette::Highlight, QColor(42, 130, 218)); p.setColor(QPalette::HighlightedText, Qt::black); p.setColor(QPalette::Disabled, QPalette::HighlightedText, disabledColor); p.setBrush(QPalette::Active, QPalette::Highlight, QColor(42, 130, 218)); p.setBrush(QPalette::Inactive, QPalette::Highlight, QColor(42, 130, 218)); return p; } ================================================ FILE: src/app/events.cpp ================================================ #include "events.h" void Events::registerLoggerForConnection(RedisClient::Connection& c) { auto self = sharedFromThis().toWeakRef(); QObject::connect( &c, &RedisClient::Connection::log, this, [self](const QString& info) { if (!self) return; emit self.toStrongRef()->log(QString("Connection: %1").arg(info)); }, Qt::QueuedConnection); QObject::connect( &c, &RedisClient::Connection::error, this, [self](const QString& error) { if (!self) return; emit self.toStrongRef()->log(QString("Connection: %1").arg(error)); }, Qt::QueuedConnection); } ================================================ FILE: src/app/events.h ================================================ #pragma once #include #include #include #include #include #include #include #include "modules/bulk-operations/bulkoperationsmanager.h" #include "common/callbackwithowner.h" namespace ConnectionsTree { class KeyItem; class TreeItem; } class Events : public QObject, public QEnableSharedFromThis { Q_OBJECT public: void registerLoggerForConnection(RedisClient::Connection& c); signals: // Tabs void openValueTab(QSharedPointer connection, QSharedPointer key, bool inNewTab); void openConsole(QSharedPointer connection, int dbIndex, bool inNewTab, QList initCmd = QList()); void openServerStats(QSharedPointer connection); void closeDbKeys(QSharedPointer connection, int dbIndex, const QRegExp& filter = QRegExp("*", Qt::CaseSensitive, QRegExp::Wildcard)); // Dialogs void requestBulkOperation( QSharedPointer connection, int dbIndex, BulkOperations::Manager::Operation op, QRegExp keyPattern, BulkOperations::AbstractOperation::OperationCallback callback); void newKeyDialog( QSharedPointer connection, QSharedPointer> callback, int dbIndex, QString keyPrefix); // Notifications void error(const QString& msg); void log(const QString& msg); void appRendered(); void pythonLoaded(); void externalFormattersLoaded(); }; ================================================ FILE: src/app/jsonutils.cpp ================================================ #include "jsonutils.h" #include #include // Based on https://github.com/nlohmann/json/blob/ec7a1d834773f9fee90d8ae908a0c9933c5646fc/src/json.hpp#L4604-L4697 // Copyright © 2013-2015 Niels Lohmann. // The code is licensed under the MIT License std::size_t extra_space(const std::string_view& s) noexcept { std::size_t result = 0; for (const auto& c : s) { switch (c) { case '"': case '\\': case '\b': case '\f': case '\n': case '\r': case '\t': { // from c (1 byte) to \x (2 bytes) result += 1; break; } default: { if (c >= 0x00 and c <= 0x1f) { // from c (1 byte) to \uxxxx (6 bytes) result += 5; } break; } } } return result; } std::string escape_string(const std::string_view& s) noexcept { const auto space = extra_space(s); if (space == 0) { return std::string(s); } // create a result string of necessary size std::string result(s.size() + space, '\\'); std::size_t pos = 0; for (const auto& c : s) { switch (c) { // quotation mark (0x22) case '"': { result[pos + 1] = '"'; pos += 2; break; } // reverse solidus (0x5c) case '\\': { // nothing to change pos += 2; break; } // backspace (0x08) case '\b': { result[pos + 1] = 'b'; pos += 2; break; } // formfeed (0x0c) case '\f': { result[pos + 1] = 'f'; pos += 2; break; } // newline (0x0a) case '\n': { result[pos + 1] = 'n'; pos += 2; break; } // carriage return (0x0d) case '\r': { result[pos + 1] = 'r'; pos += 2; break; } // horizontal tab (0x09) case '\t': { result[pos + 1] = 't'; pos += 2; break; } default: { if (c >= 0x00 and c <= 0x1f) { // print character c as \uxxxx std::snprintf(&result[pos + 1], 7, "u%04x", int(c)); pos += 6; // overwrite trailing null character result[pos] = '\\'; } else { // all other characters are added as-is result[pos++] = c; } break; } } } return result; } QByteArray escapeJsonKey(std::string_view key) { return QByteArray::fromStdString(escape_string(key)); } void print_json(QByteArray &result, simdjson::ondemand::value element, long level, bool objectValue = false) { using namespace simdjson::ondemand; QByteArray whitespace = QByteArray().fill(' ', level * 2); bool add_comma; if (!objectValue) result.append(whitespace); switch (element.type()) { case json_type::array: result.append("[\n"); add_comma = false; for (auto child : element.get_array()) { if (add_comma) { result.append(",\n"); } print_json(result, child.value(), level + 1); add_comma = true; } result.append('\n'); result.append(whitespace); result.append("]"); break; case json_type::object: result.append("{\n"); add_comma = false; for (auto field : element.get_object()) { if (add_comma) { result.append(",\n"); } result.append(whitespace); result.append(" "); result.append(QString("\"%1\": ") .arg(QString::fromUtf8(escapeJsonKey(field.unescaped_key()))) .toUtf8()); print_json(result, field.value(), level + 1, true); add_comma = true; } result.append('\n'); result.append(whitespace); result.append("}"); break; case json_type::number: result.append(QByteArray::fromStdString( std::string(std::string_view(element.raw_json_token()))) .trimmed()); break; case json_type::string: result.append(QByteArray::fromStdString( std::string(std::string_view(element.raw_json_token()))) .trimmed()); break; case json_type::boolean: result.append(bool(element) ? "true" : "false"); break; case json_type::null: result.append("null"); break; } } QByteArray JSONUtils::prettyPrintJSON(QByteArray val) { QByteArray result; result.reserve(val.size() * 32); val.resize(val.size() + simdjson::SIMDJSON_PADDING); simdjson::ondemand::parser p; try { auto doc = p.iterate(val.data(), val.size()); if (doc.is_scalar()) { return val; } print_json(result, simdjson::ondemand::value(doc), 0); } catch (const std::exception& e) { qDebug() << "Cannot parse JSON:" << e.what(); return QByteArray(); } return result; } QByteArray JSONUtils::minifyJSON(const QByteArray &val) { QByteArray minified; minified.resize(val.size()); size_t new_length{}; auto error = simdjson::minify(val.data(), val.size(), minified.data(), new_length); if (error != 0) { qDebug() << "Failed to minify JSON with simdjson:" << error; return QByteArray(); } minified.resize(new_length); return minified; } bool JSONUtils::isJSON(QByteArray val) { int originalSize = val.size(); val.resize(val.size() + simdjson::SIMDJSON_PADDING); simdjson::dom::parser parser; simdjson::dom::element data; auto error = parser.parse(val.data(), originalSize, false).get(data); // NOTE(u_glide): Workaround to distinguish invalid JSON and valid JSON with Big Int if (error == simdjson::NUMBER_ERROR) { simdjson::ondemand::parser p; try { auto doc = p.iterate(val.data(), val.size()); return !doc.is_scalar(); } catch (const std::exception& e) { qDebug() << "JSON is not valid:" << e.what(); return false; } } else if (error != simdjson::SUCCESS) { qDebug() << "JSON is not valid:" << simdjson::error_message(error); return false; } return true; } ================================================ FILE: src/app/jsonutils.h ================================================ #pragma once #include namespace JSONUtils { QByteArray prettyPrintJSON(QByteArray val); bool isJSON(QByteArray val); QByteArray minifyJSON(const QByteArray& val); }; // namespace JSONUtils ================================================ FILE: src/app/models/configmanager.cpp ================================================ #include "configmanager.h" #include #include #include #include #include #include #include #include #include #include #include "app/models/connectionconf.h" ConfigManager::ConfigManager(const QString &basePath) : m_basePath(basePath) {} QString ConfigManager::getApplicationConfigPath(const QString &configFile, bool checkPath) { QString configDir = getConfigPath(m_basePath); QDir settingsPath(configDir); if (!settingsPath.exists() && settingsPath.mkpath(configDir)) { qDebug() << "Config Dir created"; } QString configPath = QString("%1/%2").arg(configDir).arg(configFile); if (checkPath && !chechPath(configPath)) return QString(); return configPath; } bool ConfigManager::chechPath(const QString &configPath) { QFile testConfig(configPath); QFileInfo checkPermissions(configPath); if (!testConfig.exists() && testConfig.open(QIODevice::ReadWrite)) testConfig.close(); if (checkPermissions.isWritable()) { setPermissions(testConfig); return true; } return false; } void ConfigManager::setPermissions(QFile &file) { #ifdef Q_OS_WIN extern Q_CORE_EXPORT int qt_ntfs_permission_lookup; qt_ntfs_permission_lookup++; #endif if (!file.setPermissions(QFile::ReadUser | QFile::WriteUser)) qWarning() << "Cannot set permissions for config folder"; #ifdef Q_OS_WIN qt_ntfs_permission_lookup--; #endif } QString ConfigManager::getConfigPath(QString basePath) { QString configDir; #ifdef Q_OS_MACX if (basePath == QDir::homePath()) { configDir = "/Library/Preferences/rdm/"; } else { configDir = ".rdm"; } configDir = QDir::toNativeSeparators(QString("%1/%2").arg(basePath).arg(configDir)); #else configDir = QDir::toNativeSeparators(QString("%1/%2").arg(basePath).arg(".rdm")); #endif return configDir; } bool saveJsonArrayToFile(const QJsonArray &c, const QString &f) { QJsonDocument config(c); QFile confFile(f); if (confFile.open(QIODevice::WriteOnly)) { QTextStream outStream(&confFile); outStream.setCodec("UTF-8"); outStream << config.toJson(); confFile.close(); return true; } return false; } ================================================ FILE: src/app/models/configmanager.h ================================================ #pragma once #include #include #include #include class ConfigManager { public: explicit ConfigManager(const QString& basePath = QDir::homePath()); QString getApplicationConfigPath(const QString &, bool checkPath=true); public: static QString getConfigPath(QString basePath = QDir::homePath()); private: static bool chechPath(const QString&); static void setPermissions(QFile&); private: QString m_basePath; }; bool saveJsonArrayToFile(const QJsonArray& c, const QString& f); ================================================ FILE: src/app/models/connectionconf.cpp ================================================ #include "connectionconf.h" ServerConfig::ServerConfig(const QString &host, const QString &auth, const uint port, const QString &name) : RedisClient::ConnectionConfig(host, auth, port, name), m_owner(QSharedPointer()) { } ServerConfig::ServerConfig(const QVariantHash &options) : RedisClient::ConnectionConfig(options), m_owner(QSharedPointer()) { } QString ServerConfig::keysPattern() const { return param("keys_pattern", QString(DEFAULT_KEYS_GLOB_PATTERN)); } void ServerConfig::setKeysPattern(QString keyGlobPattern) { setParam("keys_pattern", keyGlobPattern); } QString ServerConfig::namespaceSeparator() const { return param("namespace_separator", QString(DEFAULT_NAMESPACE_SEPARATOR)); } void ServerConfig::setNamespaceSeparator(QString ns) { return setParam("namespace_separator", ns); } uint ServerConfig::databaseScanLimit() const { return param("db_scan_limit", DEFAULT_DB_SCAN_LIMIT); } void ServerConfig::setDatabaseScanLimit(uint limit) { setParam("db_scan_limit", limit); } bool ServerConfig::useSshTunnel() const { return RedisClient::ConnectionConfig::useSshTunnel(); } QWeakPointer ServerConfig::owner() const { return m_owner; } void ServerConfig::setOwner(QWeakPointer o) { m_owner = o; } QVariantMap ServerConfig::filterHistory() { return param("filter_history"); } void ServerConfig::setFilterHistory(QVariantMap filterHistory) { setParam("filter_history", filterHistory); } bool ServerConfig::askForSshPassword() const { return param("ask_ssh_password", false); } void ServerConfig::setAskForSshPassword(bool v) { setParam("ask_ssh_password", v); } QString ServerConfig::defaultFormatter() const { return param("default_formatter", QString("auto")); } void ServerConfig::setDefaultFormatter(const QString &v) { setParam("default_formatter", v); } QString ServerConfig::iconColor() const { return param("icon_color", QString("")); } void ServerConfig::setIconColor(const QString &v) { setParam("icon_color", v); } ================================================ FILE: src/app/models/connectionconf.h ================================================ #pragma once #include #include class TreeOperations; class ServerConfig : public RedisClient::ConnectionConfig { Q_GADGET /* Basic settings */ Q_PROPERTY(QString name READ name WRITE setName) Q_PROPERTY(QString host READ host WRITE setHost) Q_PROPERTY(uint port READ port WRITE setPort) Q_PROPERTY(QString auth READ auth WRITE setAuth) Q_PROPERTY(QString username READ username WRITE setUsername) /* SSL settings */ Q_PROPERTY(bool sslEnabled READ useSsl WRITE setSsl) Q_PROPERTY(QString sslLocalCertPath READ sslLocalCertPath WRITE setSslLocalCertPath) Q_PROPERTY(QString sslPrivateKeyPath READ sslPrivateKeyPath WRITE setSslPrivateKeyPath) Q_PROPERTY(QString sslCaCertPath READ sslCaCertPath WRITE setSslCaCertPath) /* SSH Settings */ Q_PROPERTY(QString sshPassword READ sshPassword WRITE setSshPassword) Q_PROPERTY(bool askForSshPassword READ askForSshPassword WRITE setAskForSshPassword) Q_PROPERTY(QString sshUser READ sshUser WRITE setSshUser) Q_PROPERTY(QString sshHost READ sshHost WRITE setSshHost) Q_PROPERTY(uint sshPort READ sshPort WRITE setSshPort) Q_PROPERTY(QString sshPrivateKey READ getSshPrivateKeyPath WRITE setSshPrivateKeyPath) Q_PROPERTY(bool sshAgent READ sshAgent WRITE setSshAgent) Q_PROPERTY(QString sshAgentPath READ sshAgentPath WRITE setSshAgentPath) /* Advanced settings */ Q_PROPERTY(QString keysPattern READ keysPattern WRITE setKeysPattern) Q_PROPERTY(QString namespaceSeparator READ namespaceSeparator WRITE setNamespaceSeparator) Q_PROPERTY(uint executeTimeout READ executeTimeout WRITE setExecutionTimeout) Q_PROPERTY(uint connectionTimeout READ connectionTimeout WRITE setConnectionTimeout) Q_PROPERTY(bool overrideClusterHost READ overrideClusterHost WRITE setClusterHostOverride) Q_PROPERTY(bool ignoreSSLErrors READ ignoreAllSslErrors WRITE setIgnoreAllSslErrors) Q_PROPERTY(uint databaseScanLimit READ databaseScanLimit WRITE setDatabaseScanLimit) Q_PROPERTY(QString defaultFormatter READ defaultFormatter WRITE setDefaultFormatter) Q_PROPERTY(QString iconColor READ iconColor WRITE setIconColor) public: static const char DEFAULT_NAMESPACE_SEPARATOR = ':'; static const char DEFAULT_KEYS_GLOB_PATTERN = '*'; static const bool DEFAULT_LUA_KEYS_LOADING = false; static const uint DEFAULT_DB_SCAN_LIMIT = 20; static constexpr const char* SSH_SECRET_ID = "ssh_password"; public: ServerConfig(const QString & host = "127.0.0.1", const QString & auth = "", const uint port = DEFAULT_REDIS_PORT, const QString & name = ""); explicit ServerConfig(const QVariantHash& options); QString keysPattern() const; void setKeysPattern(QString keyGlobPattern); QString namespaceSeparator() const; void setNamespaceSeparator(QString); void setLuaKeysLoading(bool); uint databaseScanLimit() const; void setDatabaseScanLimit(uint limit); Q_INVOKABLE bool useSshTunnel() const; QWeakPointer owner() const; void setOwner(QWeakPointer o); QVariantMap filterHistory(); void setFilterHistory(QVariantMap filterHistory); bool askForSshPassword() const; void setAskForSshPassword(bool v); QString defaultFormatter() const; void setDefaultFormatter(const QString& v); QString iconColor() const; void setIconColor(const QString& v); private: QWeakPointer m_owner; }; Q_DECLARE_METATYPE(ServerConfig) ================================================ FILE: src/app/models/connectiongroup.cpp ================================================ #include "connectiongroup.h" #include "connections-tree/items/servergroup.h" ConnectionGroup::ConnectionGroup(QSharedPointer g) : m_group(g) {} ConnectionGroup::ConnectionGroup() : m_group(nullptr) {} QString ConnectionGroup::name() const { if (!m_group) return QString(); return m_group->getDisplayName(); } void ConnectionGroup::setName(const QString &n) { if (m_group) m_group->setName(n); } QSharedPointer ConnectionGroup::serverGroup() const { return m_group; } ================================================ FILE: src/app/models/connectiongroup.h ================================================ #pragma once #include #include namespace ConnectionsTree { class ServerGroup; } class ConnectionGroup { Q_GADGET Q_PROPERTY(QString name READ name WRITE setName) public: ConnectionGroup(); ConnectionGroup(QSharedPointer g); QString name() const; void setName(const QString& n); QSharedPointer serverGroup() const; private: QSharedPointer m_group; }; Q_DECLARE_METATYPE(ConnectionGroup) ================================================ FILE: src/app/models/connectionsmanager.cpp ================================================ #include "connectionsmanager.h" #include #include #include #include #include #include #include "app/events.h" #include "configmanager.h" #include "modules/bulk-operations/bulkoperationsmanager.h" #include "modules/connections-tree/items/serveritem.h" #include "modules/connections-tree/items/servergroup.h" #include "modules/value-editor/tabsmodel.h" ConnectionsManager::ConnectionsManager(const QString& configPath, QSharedPointer events) : ConnectionsTree::Model(), m_configPath(configPath), m_events(events) { connect(this, &ConnectionsTree::Model::error, m_events.data(), &Events::error); } void ConnectionsManager::loadConnections() { if (!m_configPath.isEmpty() && QFile::exists(m_configPath)) { loadConnectionsConfigFromFile(m_configPath); } emit connectionsLoaded(); } void ConnectionsManager::addNewConnection( const ServerConfig& config, bool saveToConfig, QSharedPointer group) { createServerItemForConnection(config, group); if (saveToConfig) saveConfig(); buildConnectionsCache(); } void ConnectionsManager::addNewGroup(const QString& name) { auto group = QSharedPointer( new ConnectionsTree::ServerGroup( name, *static_cast(this))); addGroup(group); saveConfig(); } void ConnectionsManager::updateGroup(const ConnectionGroup &group) { auto serverGroup = group.serverGroup(); if (!serverGroup){ qWarning() << "invalid server group"; return; } emit itemChanged(serverGroup); saveConfig(); buildConnectionsCache(); } void ConnectionsManager::updateConnection(const ServerConfig& config) { if (!config.owner()) return addNewConnection(config); auto treeOperations = config.owner().toStrongRef(); if (!treeOperations) return; treeOperations->setConfig(config); saveConfig(); } bool ConnectionsManager::importConnections(const QString& path) { if (loadConnectionsConfigFromFile(path, true)) { emit sizeChanged(); return true; } return false; } bool ConnectionsManager::loadConnectionsConfigFromFile(const QString& config, bool saveChangesToFile) { QJsonArray connections; QFile conf(config); if (!conf.open(QIODevice::ReadOnly)) return false; QByteArray data = conf.readAll(); conf.close(); QJsonDocument jsonConfig = QJsonDocument::fromJson(data); if (jsonConfig.isEmpty()) return true; if (!jsonConfig.isArray()) { return false; } connections = jsonConfig.array(); for (QJsonValue connection : connections) { if (!connection.isObject()) continue; auto obj = connection.toObject(); bool isValidGroup = obj.contains("type") && obj.contains("connections") && obj.contains("name") && obj["connections"].isArray() && obj["type"].toString().toLower() == "group"; if (isValidGroup) { auto groupConnections = obj["connections"].toArray(); auto group = QSharedPointer( new ConnectionsTree::ServerGroup( obj["name"].toString(), *static_cast(this))); for (const QJsonValue &c : qAsConst(groupConnections)) { if (!c.isObject()) continue; ServerConfig conf(c.toObject().toVariantHash()); if (conf.isNull()) continue; conf.setId(QUuid::createUuid().toByteArray()); addNewConnection(conf, false, group); } addGroup(group); } else { ServerConfig conf(obj.toVariantHash()); if (conf.isNull()) continue; addNewConnection(conf, false); } } if (saveChangesToFile) saveConfig(); buildConnectionsCache(); return true; } void ConnectionsManager::tryToConnect(const ServerConfig &config, QJSValue jsCallback) { RedisClient::Connection testConnection(config); m_events->registerLoggerForConnection(testConnection); try { jsCallback.call(QJSValueList{testConnection.connect()}); } catch (const RedisClient::Connection::Exception&) { jsCallback.call(QJSValueList{false}); } } void ConnectionsManager::saveConfig() { saveConnectionsConfigToFile(m_configPath); } bool ConnectionsManager::saveConnectionsConfigToFile( const QString& pathToFile) { QJsonArray connections; auto addConfig = [](QSharedPointer i, QJsonArray& connections) { auto srvItem = i.dynamicCast(); if (!srvItem) return; auto op = srvItem->getOperations().dynamicCast(); if (!op) return; auto config = op->config(); QSet ignoreFields {"id"}; if (config.askForSshPassword()) { ignoreFields.insert(ServerConfig::SSH_SECRET_ID); } connections.push_back(QJsonValue(config.toJsonObject(ignoreFields))); }; for (auto item : m_treeItems) { if (item->type() == "server_group") { QJsonObject group; group["type"] = "group"; group["name"] = item->getDisplayName(); QJsonArray groupConnections; for (auto srv : item->getAllChilds()) { addConfig(srv, groupConnections); } group["connections"] = groupConnections; connections.push_back(QJsonValue(group)); } else if (item->type() == "server") { addConfig(item, connections); } } return saveJsonArrayToFile(connections, pathToFile); } void ConnectionsManager::testConnectionSettings(const ServerConfig& config, QJSValue jsCallback) { if (!jsCallback.isCallable()) { qDebug() << "JS callback is not callable"; return; } if (config.askForSshPassword()) { m_jsCallback = jsCallback; emit askUserForConnectionSecret(config, ServerConfig::SSH_SECRET_ID); } else { tryToConnect(config, jsCallback); } } void ConnectionsManager::proceedWithConnectionSecret(const ServerConfig &config) { if (m_jsCallback.isCallable()) { tryToConnect(config, m_jsCallback); m_jsCallback = QJSValue(); return; } if (!config.owner()) { qWarning() << "Invalid config with secret"; return; } auto treeOperations = config.owner().toStrongRef(); if (!treeOperations) { qWarning() << "Config with secret doesn't have owner"; return; } treeOperations->proceedWithSecret(config); } ServerConfig ConnectionsManager::createEmptyConfig() const { return ServerConfig(); } ServerConfig ConnectionsManager::parseConfigFromRedisConnectionString(const QString& connectionString) const { QUrl url = QUrl(connectionString); QUrlQuery query = QUrlQuery(url.query()); ServerConfig config; config.setHost(url.host().isEmpty() || url.host() == "localhost" ? "127.0.0.1" : url.host()); config.setPort(url.port() == -1 ? 6379 : url.port()); config.setUsername(url.userName()); config.setAuth(url.password().isEmpty() ? query.queryItemValue("password") : url.password()); if (url.scheme() == "rediss" || (!query.isEmpty() && query.queryItemValue("ssl") == "true")) { config.setSsl(true); } return config; } bool ConnectionsManager::isRedisConnectionStringValid( const QString& connectionString) { QUrl url; if (connectionString.startsWith("redis://") || connectionString.startsWith("rediss://")) { url = QUrl(connectionString); } else { url = QUrl(QString("redis://%1").arg(connectionString)); } return url.isValid() && (url.scheme() == "redis" || url.scheme() == "rediss") && !url.host().isEmpty(); } int ConnectionsManager::size() { int connectionsCount = 0; for (auto item : qAsConst(m_treeItems)) { if (item->type() == "server_group") { connectionsCount += item->childCount(); } else if (item->type() == "server") { connectionsCount++; } } return connectionsCount; } QSharedPointer ConnectionsManager::getByIndex( int index) { auto op = m_connectionsCache.values().at(index)->getOperations(); if (!op) return QSharedPointer(); auto treeOp = op.dynamicCast(); if (!treeOp) return QSharedPointer(); return treeOp->connection(); } QStringList ConnectionsManager::getConnections() { return m_connectionsCache.keys(); } void ConnectionsManager::applyGroupChanges() { ConnectionsTree::Model::applyGroupChanges(); buildConnectionsCache(); saveConfig(); } void ConnectionsManager::createServerItemForConnection( const ServerConfig& config, QSharedPointer group) { using namespace ConnectionsTree; auto treeModel = QSharedPointer(new TreeOperations(config, m_events)); connect(treeModel.data(), &TreeOperations::createNewConnection, this, [this](const ServerConfig& config) { addNewConnection(config); }); connect(treeModel.data(), &TreeOperations::secretRequired, this, &ConnectionsManager::askUserForConnectionSecret); QWeakPointer parent; if (group) { parent = group.toWeakRef(); } auto serverItem = QSharedPointer( new ServerItem(treeModel.dynamicCast(), *static_cast(this), parent)); serverItem->setWeakPointer(serverItem.toWeakRef()); connect( treeModel.data(), &TreeOperations::configUpdated, this, [this, serverItem]() { if (!serverItem) return; emit itemChanged( serverItem.dynamicCast().toWeakRef()); }); connect(treeModel.data(), &TreeOperations::filterHistoryUpdated, this, [this]() { saveConfig(); }); connect(serverItem.data(), &ConnectionsTree::ServerItem::editActionRequested, this, [this, treeModel]() { if (!treeModel) return; auto config = treeModel->config(); emit connectionAboutToBeEdited(config.name()); // NOTE(u_glide): Do not show temproary stored password in the UI if (config.askForSshPassword()) { config.setSshPassword(QString()); } emit editConnection(config); }); connect(serverItem.data(), &ConnectionsTree::ServerItem::deleteActionRequested, this, [this, serverItem, treeModel, group]() { if (!serverItem || !treeModel) return; emit connectionAboutToBeEdited(treeModel->config().name()); if (group) { group->removeConnection(serverItem); } else { removeRootItem(serverItem); } buildConnectionsCache(); emit sizeChanged(); saveConfig(); }); if (group) { group->addServer(serverItem); } else { addRootItem(serverItem); } } void ConnectionsManager::addGroup( QSharedPointer group) { connect(group.data(), &ConnectionsTree::ServerGroup::editActionRequested, this, [this, group]() { if (!group) return; ConnectionGroup g(group); emit editConnectionGroup(g); }); connect(group.data(), &ConnectionsTree::ServerGroup::deleteActionRequested, this, [this, group]() { if (!group) return; removeRootItem(group); buildConnectionsCache(); emit sizeChanged(); saveConfig(); }); addRootItem(group); buildConnectionsCache(); } void ConnectionsManager::buildConnectionsCache() { m_connectionsCache.clear(); for (auto item : m_treeItems) { if (item->type() == "server_group") { QString nameTemplate = QString("[%1] %2").arg(item->getDisplayName()); for (auto srv : item->getAllChilds()) { QString name = nameTemplate.arg(srv->getDisplayName()); m_connectionsCache[name] = srv.dynamicCast(); } } else if (item->type() == "server") { m_connectionsCache[item->getDisplayName()] = item.dynamicCast(); } } } ================================================ FILE: src/app/models/connectionsmanager.h ================================================ #pragma once #include #include #include #include "app/models/connectionconf.h" #include "bulk-operations/connections.h" #include "connections-tree/model.h" #include "treeoperations.h" #include "connectiongroup.h" namespace ValueEditor { class TabsModel; } namespace ConnectionsTree { class ServerGroup; } class Events; class ConnectionsManager : public ConnectionsTree::Model, public BulkOperations::ConnectionsModel { Q_OBJECT Q_PROPERTY(int connectionsCount READ size NOTIFY sizeChanged) public: ConnectionsManager(const QString& m_configPath, QSharedPointer events); void loadConnections(); Q_INVOKABLE void addNewConnection(const ServerConfig& config, bool saveToConfig = true, QSharedPointer group = QSharedPointer()); Q_INVOKABLE void addNewGroup(const QString& name); Q_INVOKABLE void updateGroup(const ConnectionGroup& group); Q_INVOKABLE void updateConnection(const ServerConfig& config); Q_INVOKABLE bool importConnections(const QString&); Q_INVOKABLE bool saveConnectionsConfigToFile(const QString&); Q_INVOKABLE void testConnectionSettings(const ServerConfig& config, QJSValue jsCallback); Q_INVOKABLE void proceedWithConnectionSecret(const ServerConfig& config); Q_INVOKABLE ServerConfig createEmptyConfig() const; Q_INVOKABLE ServerConfig parseConfigFromRedisConnectionString(const QString&) const; Q_INVOKABLE bool isRedisConnectionStringValid(const QString&); void saveConfig(); Q_INVOKABLE int size() override; // BulkOperations model methods QSharedPointer getByIndex(int index) override; QStringList getConnections() override; void applyGroupChanges() override; signals: void editConnection(ServerConfig config); void editConnectionGroup(ConnectionGroup group); void connectionAboutToBeEdited(QString name); void sizeChanged(); void connectionsLoaded(); void askUserForConnectionSecret(const ServerConfig& config, const QString& id); protected: bool loadConnectionsConfigFromFile(const QString& config, bool saveChangesToFile = false); void tryToConnect(const ServerConfig& config, QJSValue jsCallback); private: void createServerItemForConnection(const ServerConfig &config, QSharedPointer group=QSharedPointer()); void addGroup(QSharedPointer serverGroup); void buildConnectionsCache(); private: QString m_configPath; QSharedPointer m_events; QJSValue m_jsCallback; QMap> m_connectionsCache; }; ================================================ FILE: src/app/models/key-models/abstractkey.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include "modules/value-editor/keymodel.h" #include "rowcache.h" #include "app/models/connectionconf.h" template class KeyModel : public ValueEditor::Model { public: KeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl, QByteArray rowsCountCmd = QByteArray(), QByteArray rowsLoadCmd = QByteArray()) : m_connection(connection), m_keyFullPath(fullPath), m_dbIndex(dbIndex), m_ttl(ttl), m_rowCount(0), m_isMultiRow(!rowsCountCmd.isEmpty()), m_rowsCountCmd(rowsCountCmd), m_rowsLoadCmd(rowsLoadCmd), m_scanCursor(0), m_notifier(new ValueEditor::ModelSignals(), &QObject::deleteLater) {} virtual QString getKeyName() override { return printableString(m_keyFullPath); } virtual QString getKeyTitle(int limit=-1) override { QString fullTitle = QString("%1::db%2::%3") .arg(m_connection->getConfig().name()) .arg(m_dbIndex) .arg(getKeyName()); int length = fullTitle.size(); if (limit == -1 || length <= limit){ return fullTitle; } else { return QString("%1 ... %2").arg(fullTitle.mid(0, limit/2)).arg(fullTitle.mid(length - limit/2)); } } virtual long long getTTL() override { return m_ttl; } virtual bool isMultiRow() const override { return m_isMultiRow; } virtual bool isRowLoaded(int rowIndex) override { return m_rowsCache.isRowLoaded(rowIndex); } virtual unsigned long rowsCount() override { if (isMultiRow()) return m_rowCount; else return 1; } virtual void setKeyName(const QByteArray& newKeyName, ValueEditor::Model::Callback c) override { // NOTE(u_glide): DUMP + RESTORE + DEL is cluster compatible alternative to RENAME command executeCmd( {"DUMP", m_keyFullPath}, c, [this, newKeyName](RedisClient::Response r, Callback c) { executeCmd( {"RESTORE", newKeyName, QString::number(m_ttl > 0 ? m_ttl : 0).toLatin1(), r.value().toByteArray()}, c, [this, newKeyName](RedisClient::Response r, Callback c) { if (!r.isOkMessage()) { return c(QCoreApplication::translate( "RESP", "Cannot rename key %1: %2") .arg(getKeyName()) .arg(r.value().toString())); } executeCmd( {"DEL", m_keyFullPath}, [](const QString&) {}, [](RedisClient::Response, Callback) {}); m_keyFullPath = newKeyName; c(QString()); }, RedisClient::Response::Type::Status); }, RedisClient::Response::Type::String); } virtual void setTTL(const long long ttl, ValueEditor::Model::Callback c) override { executeCmd( {"EXPIRE", m_keyFullPath, QString::number(ttl).toLatin1()}, c, [this, ttl](RedisClient::Response r, Callback c) { if (r.value().toInt() == 0) { return c( QCoreApplication::translate("RESP", "Cannot set TTL for key %1") .arg(getKeyName())); } if (ttl >= 0) m_ttl = ttl; else m_ttl = -1; c(QString()); }, RedisClient::Response::Type::Integer); } virtual void persistKey(Callback c) override { executeCmd( {"PERSIST", m_keyFullPath}, c, [this](RedisClient::Response r, Callback c) { if (r.value().toInt() == 0) { return c(QCoreApplication::translate( "RESP", "Cannot persist key '%1'.
Key does not exist or " "does not have an assigned TTL value") .arg(getKeyName())); } m_ttl = -1; c(QString()); }, RedisClient::Response::Type::Integer); } virtual void removeKey(ValueEditor::Model::Callback c) override { executeCmd({"DEL", m_keyFullPath}, c, [this](RedisClient::Response, Callback c) { m_notifier->removed(); c(QString()); }); } virtual void loadRows(QVariant rowStart, unsigned long count, LoadRowsCallback callback) override { if (m_rowsLoadCmd.mid(1, 4).toLower() == "scan") { QList cmdParts = {m_rowsLoadCmd, m_keyFullPath, QString::number(m_scanCursor).toLatin1(), "COUNT", QString::number(count).toLatin1()}; auto self = ValueEditor::Model::sharedFromThis().toWeakRef(); m_connection->cmd( cmdParts, m_notifier.data(), -1, [this, callback, rowStart, self](RedisClient::Response r) { if (!r.isValidScanResponse()) { callback(QCoreApplication::translate( "RESP", "Cannot parse scan response"), 0); return; } if (r.getCursor() > 0) { m_scanCursor = r.getCursor(); } try { unsigned long addedRows = addLoadedRowsToCache(r.getCollection(), rowStart); callback(QString(), addedRows); } catch (const std::runtime_error& e) { callback(QString(e.what()), 0); } }, [self, callback](QString err) { if (!self) { return; } return callback( QCoreApplication::translate("RESP", "Connection error: ") + err, 0); }); } else { getRowsRange( getRangeCmd(rowStart, count), [this, callback, rowStart](const QString& err, QVariantList result) { if (!err.isEmpty()) return callback(err, 0); unsigned long addedRows = addLoadedRowsToCache(result, rowStart); callback(QString(), addedRows); }); } } virtual void clearRowCache() override { m_rowsCache.clear(); } virtual QSharedPointer getConnector() const override { return m_notifier; } virtual QSharedPointer getConnection() const override { return m_connection; } virtual QString getDefaultFormatter() const override{ if (!m_connection) return QString("auto"); // TODO(u_glide): Pass ServerConfig to KeyModel and remove this return m_connection->getConfig().getInternalParameters().value("default_formatter", QString("auto")).toString(); } virtual unsigned int dbIndex() const override { return m_dbIndex; } virtual void loadRowsCount(ValueEditor::Model::Callback c) override { if (!isMultiRow()) { m_rowCount = 1; return c(QString()); } executeCmd( {m_rowsCountCmd, m_keyFullPath}, c, [this](RedisClient::Response r, Callback c) { m_rowCount = r.value().toUInt(); c(QString()); }, RedisClient::Response::Type::Integer); } protected: // multi row internal operations virtual QList getRangeCmd(QVariant rowStartId, unsigned long count) { QList cmd; unsigned long rowStart = rowStartId.toULongLong(); unsigned long rowEnd = std::min(m_rowCount, rowStart + count) - 1; if (m_rowsLoadCmd.contains(' ')) { QList suffixCmd(m_rowsLoadCmd.split(' ')); cmd << suffixCmd.takeFirst(); cmd << m_keyFullPath << QString::number(rowStart).toLatin1() << QString::number(rowEnd).toLatin1(); cmd += suffixCmd; } else { cmd << m_rowsLoadCmd << m_keyFullPath << QString::number(rowStart).toLatin1() << QString::number(rowEnd).toLatin1(); } return cmd; } virtual void getRowsRange( const QList& rangeCmd, std::function callback) { try { m_connection->command( rangeCmd, getConnector().data(), [this, callback](RedisClient::Response r, QString e) { if (!e.isEmpty()) { return callback(e, QVariantList()); } if (r.type() != RedisClient::Response::Array) { return callback(QCoreApplication::translate( "RESP", "Cannot load rows for key %1: %2") .arg(getKeyName()), QVariantList()); } return callback(QString(), r.value().toList()); }, -1); } catch (const RedisClient::Connection::Exception& e) { callback( QCoreApplication::translate("RESP", "Cannot load rows for key %1: %2") .arg(getKeyName()) .arg(e.what()), QVariantList()); } } // row validator virtual bool isRowValid(const QVariantMap& row) { if (row.isEmpty()) return false; QSet validKeys; foreach (QByteArray role, getRoles().values()) { validKeys.insert(role); } QMapIterator i(row); while (i.hasNext()) { i.next(); if (!validKeys.contains(i.key())) return false; } return true; } virtual void setRemovedIfEmpty() { if (m_rowCount == 0) { m_notifier->removed(); } } typedef std::function CmdHandler; virtual void executeCmd(QList cmd, Callback c, CmdHandler handler = CmdHandler(), RedisClient::Response::Type expectedType = RedisClient::Response::Type::Unknown) { m_connection->cmd( cmd, m_notifier.data(), -1, [c, handler, expectedType](RedisClient::Response r) { if (expectedType != RedisClient::Response::Type::Unknown && r.type() != expectedType) { return c(QCoreApplication::translate( "RESP", "Server returned unexpected response: ") + r.value().toString()); } if (handler) { return handler(r, c); } else { return c(QString()); } }, [c](QString err) { return c(QCoreApplication::translate("RESP", "Connection error: ") + err); }); } virtual int addLoadedRowsToCache(const QVariantList& rows, QVariant rowStart) = 0; QVariant filter(const QString& key) const override { return m_filters.value(key, QVariant()); }; void setFilter(const QString& k, QVariant v) override { m_filters[k] = v; qDebug() << "filter:" << k << v; } protected: QSharedPointer m_connection; QByteArray m_keyFullPath; int m_dbIndex; long long m_ttl; unsigned long m_rowCount; bool m_isMultiRow; // CMD strings QByteArray m_rowsCountCmd; QByteArray m_rowsLoadCmd; MappedCache m_rowsCache; long long m_scanCursor; QSharedPointer m_notifier; QVariantMap m_filters; }; ================================================ FILE: src/app/models/key-models/bfkey.cpp ================================================ #include "bfkey.h" #include BloomFilterKeyModel::BloomFilterKeyModel( QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl, QString filterFamily) : KeyModel(connection, fullPath, dbIndex, ttl), m_type(filterFamily) {} QString BloomFilterKeyModel::type() { return m_type; } QStringList BloomFilterKeyModel::getColumnNames() { return QStringList() << "value"; } QHash BloomFilterKeyModel::getRoles() { QHash roles; roles[Roles::Value] = "value"; return roles; } QVariant BloomFilterKeyModel::getData(int rowIndex, int dataRole) { if (rowIndex > 0 || !isRowLoaded(rowIndex)) return QVariant(); if (dataRole == Roles::Value) return QJsonDocument::fromVariant(m_rowsCache[rowIndex]) .toJson(QJsonDocument::Compact); return QVariant(); } void BloomFilterKeyModel::addRow(const QVariantMap& row, Callback c) { QByteArray value = row.value("value").toByteArray(); executeCmd({QString("%1.ADD").arg(m_type).toLatin1(), m_keyFullPath, value}, [this, c](const QString& err) { m_rowCount++; return c(err); }); } void BloomFilterKeyModel::loadRows(QVariant, unsigned long, LoadRowsCallback callback) { auto onConnectionError = [callback](const QString& err) { return callback(err, 0); }; auto responseHandler = [this, callback](const RedisClient::Response& r, Callback) { m_rowsCache.clear(); auto value = r.value().toList(); QVariantMap row; for (auto item = value.cbegin(); item != value.cend(); ++item) { auto key = item->toByteArray(); ++item; if (item == value.cend()) { emit m_notifier->error(QCoreApplication::translate( "RESP", "Data was loaded from server partially.")); break; } auto keyVal = item->toByteArray(); row[key] = keyVal; } m_rowsCache.push_back(row); callback(QString(), 1); }; executeCmd({QString("%1.INFO").arg(m_type).toLatin1(), m_keyFullPath}, onConnectionError, responseHandler, RedisClient::Response::Array); } ================================================ FILE: src/app/models/key-models/bfkey.h ================================================ #pragma once #include "stringkey.h" class BloomFilterKeyModel : public KeyModel { public: BloomFilterKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl, QString filterFamily="bf"); QString type() override; QStringList getColumnNames() override; QHash getRoles() override; QVariant getData(int rowIndex, int dataRole) override; void addRow(const QVariantMap&, Callback c) override; virtual void updateRow(int, const QVariantMap&, Callback) override { // NOTE(u_glide): BF/CF is read-only } void loadRows(QVariant, unsigned long, LoadRowsCallback callback) override; void removeRow(int, Callback) override { // NOTE(u_glide): BF/CF is read-only } virtual unsigned long rowsCount() override { return m_rowCount; } protected: int addLoadedRowsToCache(const QVariantList&, QVariant) override { return 1; } private: enum Roles { Value = Qt::UserRole + 1 }; QString m_type; }; ================================================ FILE: src/app/models/key-models/hashkey.cpp ================================================ #include "hashkey.h" #include #include HashKeyModel::HashKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl) : KeyModel(connection, fullPath, dbIndex, ttl, "HLEN", "HSCAN") {} QString HashKeyModel::type() { return "hash"; } QStringList HashKeyModel::getColumnNames() { return QStringList() << "rowNumber" << "key" << "value"; } QHash HashKeyModel::getRoles() { QHash roles; roles[Roles::RowNumber] = "rowNumber"; roles[Roles::Key] = "key"; roles[Roles::Value] = "value"; return roles; } QVariant HashKeyModel::getData(int rowIndex, int dataRole) { if (!isRowLoaded(rowIndex)) return QVariant(); QPair row = m_rowsCache[rowIndex]; if (dataRole == Roles::Key) return row.first; else if (dataRole == Roles::Value) return row.second; else if (dataRole == Roles::RowNumber) return rowIndex; return QVariant(); } void HashKeyModel::updateRow(int rowIndex, const QVariantMap &row, Callback c) { if (!isRowLoaded(rowIndex) || !isRowValid(row)) { c(QCoreApplication::translate("RESP", "Invalid row")); return; } QPair cachedRow = m_rowsCache[rowIndex]; bool keyChanged = cachedRow.first != row["key"].toByteArray(); bool valueChanged = cachedRow.second != row["value"].toByteArray(); QPair newRow( (keyChanged) ? row["key"].toByteArray() : cachedRow.first, (valueChanged) ? row["value"].toByteArray() : cachedRow.second); auto afterValueUpdate = [this, c, rowIndex, newRow](const QString &err) { if (err.isEmpty()) m_rowsCache.replace(rowIndex, newRow); return c(err); }; if (keyChanged) { deleteHashRow(cachedRow.first, [this, c, newRow, afterValueUpdate](const QString &err) { if (err.size() > 0) return c(err); setHashRow(newRow.first, newRow.second, afterValueUpdate); }); } else { setHashRow(newRow.first, newRow.second, afterValueUpdate); } } void HashKeyModel::addRow(const QVariantMap &row, Callback c) { if (!isRowValid(row)) { c(QCoreApplication::translate("RESP", "Invalid row")); return; } setHashRow( row["key"].toByteArray(), row["value"].toByteArray(), [this, c](const QString &err) { if (err.isEmpty()) m_rowCount++; return c(err); }, false); } void HashKeyModel::removeRow(int i, Callback c) { if (!isRowLoaded(i)) return; QPair row = m_rowsCache[i]; deleteHashRow(row.first, [this, i, c](const QString &err) { if (err.isEmpty()) { m_rowCount--; m_rowsCache.removeAt(i); setRemovedIfEmpty(); } return c(err); }); } void HashKeyModel::setHashRow(const QByteArray &hashKey, const QByteArray &hashValue, Callback c, bool updateIfNotExist) { QList rawCmd{(updateIfNotExist) ? "HSET" : "HSETNX", m_keyFullPath, hashKey, hashValue}; executeCmd(rawCmd, c, [updateIfNotExist](RedisClient::Response r, Callback c) { if (updateIfNotExist == false && r.value().toInt() == 0) { return c(QCoreApplication::translate( "RESP", "Value with the same key already exists")); } else { return c(QString()); } }); } void HashKeyModel::deleteHashRow(const QByteArray &hashKey, Callback c) { executeCmd({"HDEL", m_keyFullPath, hashKey}, c); } int HashKeyModel::addLoadedRowsToCache(const QVariantList &rows, QVariant rowStartId) { QList> result; for (QVariantList::const_iterator item = rows.begin(); item != rows.end(); ++item) { QPair value; value.first = item->toByteArray(); ++item; if (item == rows.end()) { emit m_notifier->error(QCoreApplication::translate( "RESP", "Data was loaded from server partially.")); return 0; } value.second = item->toByteArray(); result.push_back(value); } auto rowStart = rowStartId.toLongLong(); m_rowsCache.addLoadedRange({rowStart, rowStart + result.size() - 1}, result); return result.size(); } ================================================ FILE: src/app/models/key-models/hashkey.h ================================================ #pragma once #include "abstractkey.h" class HashKeyModel : public KeyModel> { public: HashKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl); QString type() override; QStringList getColumnNames() override; QHash getRoles() override; QVariant getData(int rowIndex, int dataRole) override; void addRow(const QVariantMap &, Callback) override; virtual void updateRow(int rowIndex, const QVariantMap &, Callback) override; void removeRow(int, Callback) override; protected: int addLoadedRowsToCache(const QVariantList &list, QVariant rowStart) override; private: enum Roles { RowNumber = Qt::UserRole + 1, Key, Value }; void setHashRow(const QByteArray &hashKey, const QByteArray &hashValue, Callback c, bool updateIfNotExist = true); void deleteHashRow(const QByteArray &hashKey, Callback c); }; ================================================ FILE: src/app/models/key-models/keyfactory.cpp ================================================ #include "keyfactory.h" #include #include #include #include #include "bfkey.h" #include "hashkey.h" #include "listkey.h" #include "rejsonkey.h" #include "setkey.h" #include "sortedsetkey.h" #include "stream.h" #include "stringkey.h" #include "unknownkey.h" KeyFactory::KeyFactory() {} void KeyFactory::loadKey( QSharedPointer connection, QByteArray keyFullPath, int dbIndex, std::function, const QString&)> callback) { auto processError = [callback, keyFullPath](const QString& err) { QString msg(QCoreApplication::translate( "RESP", "Cannot load key %1, connection error occurred: %2")); callback(QSharedPointer(), msg.arg(printableString(keyFullPath)).arg(err)); }; auto loadModel = [this, connection, keyFullPath, dbIndex, callback, processError](RedisClient::Response resp) { QSharedPointer result; if (resp.isErrorMessage() || resp.type() != RedisClient::Response::Type::Status) { QString msg(QCoreApplication::translate( "RESP", "Cannot load key %1, connection error occurred: %2")); callback( result, msg.arg(printableString(keyFullPath)).arg(resp.value().toString())); return; } QString type = resp.value().toString(); if (type == "none") { QString msg(QCoreApplication::translate( "RESP", "Cannot load key %1 because it doesn't exist in database." " Please reload connection tree and try again.")); callback(result, msg.arg(printableString(keyFullPath))); return; } auto parseTtl = [this, type, connection, keyFullPath, dbIndex, callback, processError](const RedisClient::Response& ttlResult) { long long ttl = -1; if (ttlResult.type() == RedisClient::Response::Integer) { ttl = ttlResult.value().toLongLong(); } auto result = createModel(type, connection, keyFullPath, dbIndex, ttl); callback(result, QString()); }; connection->cmd({"ttl", keyFullPath}, this, -1, parseTtl, processError); }; try { connection->cmd({"type", keyFullPath}, this, dbIndex, loadModel, processError); } catch (const RedisClient::Connection::Exception& e) { callback(QSharedPointer(), QCoreApplication::translate("RESP", "Cannot retrieve type of the key: ") + QString(e.what())); } } void KeyFactory::createNewKeyRequest( QSharedPointer connection, QSharedPointer callback, int dbIndex, QString keyPrefix) { if (connection.isNull() || dbIndex < 0) return; emit newKeyDialog(NewKeyRequest(connection, dbIndex, callback, keyPrefix)); } void KeyFactory::submitNewKeyRequest(NewKeyRequest r) { QSharedPointer result = createModel( r.keyType(), r.connection(), r.keyName().toUtf8(), r.dbIndex(), -1); if (!result) return; auto onRowAdded = [this, r, result](const QString& err) { if (err.size() > 0) { emit error(err); return; } r.callback(); emit keyAdded(); }; r.connection()->cmd( {"PING"}, this, r.dbIndex(), [onRowAdded, result, r](const RedisClient::Response& resp) { auto testResp = resp.value().toByteArray(); if (testResp != "PONG") { return onRowAdded(testResp); } auto val = r.value(); if (!r.valueFilePath().isEmpty() && QFile::exists(r.valueFilePath())) { QFile valueFile(r.valueFilePath()); if (!valueFile.open(QIODevice::ReadOnly)) { return onRowAdded(QCoreApplication::translate( "RESP", "Cannot open file with key value")); } val["value"] = valueFile.readAll(); } result->addRow(val, onRowAdded); }, onRowAdded); } QSharedPointer KeyFactory::createModel( QString type, QSharedPointer connection, QByteArray keyFullPath, int dbIndex, long long ttl) { if (type == "string") { return QSharedPointer( new StringKeyModel(connection, keyFullPath, dbIndex, ttl)); } else if (type == "list") { return QSharedPointer( new ListKeyModel(connection, keyFullPath, dbIndex, ttl)); } else if (type == "set") { return QSharedPointer( new SetKeyModel(connection, keyFullPath, dbIndex, ttl)); } else if (type == "zset") { return QSharedPointer( new SortedSetKeyModel(connection, keyFullPath, dbIndex, ttl)); } else if (type == "hash") { return QSharedPointer( new HashKeyModel(connection, keyFullPath, dbIndex, ttl)); } else if (type == "ReJSON-RL" || type == "ReJSON") { return QSharedPointer( new ReJSONKeyModel(connection, keyFullPath, dbIndex, ttl)); } else if (type == "stream") { return QSharedPointer( new StreamKeyModel(connection, keyFullPath, dbIndex, ttl)); } else if (type.startsWith("MBbloom")) { QString ff = type.endsWith("CF")? "cf" : "bf"; return QSharedPointer( new BloomFilterKeyModel(connection, keyFullPath, dbIndex, ttl, ff)); } return QSharedPointer( new UnknownKeyModel(connection, keyFullPath, dbIndex, ttl, type)); } ================================================ FILE: src/app/models/key-models/keyfactory.h ================================================ #pragma once #include #include "exception.h" #include "modules/value-editor/abstractkeyfactory.h" #include "newkeyrequest.h" #include "modules/connections-tree/operations.h" class KeyFactory : public QObject, public ValueEditor::AbstractKeyFactory { Q_OBJECT public: KeyFactory(); void loadKey( QSharedPointer connection, QByteArray keyFullPath, int dbIndex, std::function, const QString&)> callback) override; public slots: void createNewKeyRequest( QSharedPointer connection, QSharedPointer callback, int dbIndex, QString keyPrefix); void submitNewKeyRequest(NewKeyRequest r); signals: void newKeyDialog(NewKeyRequest r); void keyAdded(); void error(const QString& err); private: QSharedPointer createModel( QString type, QSharedPointer connection, QByteArray keyFullPath, int dbIndex, long long ttl); }; ================================================ FILE: src/app/models/key-models/listkey.cpp ================================================ #include "listkey.h" #include const static QByteArray LIST_ITEM_REMOVAL_STUB("---VALUE_REMOVED_BY_RESP_APP---"); ListKeyModel::ListKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl) : ListLikeKeyModel(connection, fullPath, dbIndex, ttl, "LLEN", "LRANGE") {} QString ListKeyModel::type() { return "list"; } void ListKeyModel::updateRow(int rowIndex, const QVariantMap &row, Callback c) { if (!isRowLoaded(rowIndex) || !isRowValid(row)) { return c(QCoreApplication::translate("RESP", "Invalid row")); } int dbRowIndex = rowIndex; if (isReverseOrder()) { dbRowIndex = -rowIndex - 1; } QByteArray newRow(row["value"].toByteArray()); auto afterRowUpdate = [this, rowIndex, newRow, c](const QString &err) { if (err.isEmpty()) m_rowsCache.replace(rowIndex, newRow); return c(err); }; verifyListItemPosition(dbRowIndex, [this, dbRowIndex, c, newRow, afterRowUpdate](const QString &err) { if (err.size() > 0) return c(err); setListRow(dbRowIndex, newRow, afterRowUpdate); }); } void ListKeyModel::addRow(const QVariantMap &row, Callback c) { if (!isRowValid(row)) { emit m_notifier->error(QCoreApplication::translate("RESP", "Invalid row")); return; } addListRow(row["value"].toByteArray(), [this, c](const QString &err) { if (err.isEmpty()) m_rowCount++; return c(err); }); } void ListKeyModel::removeRow(int i, ValueEditor::Model::Callback c) { if (!isRowLoaded(i)) return; auto onItemRemoval = [this, c, i](const QString &err) { if (err.isEmpty()) { m_rowCount--; m_rowsCache.removeAt(i); setRemovedIfEmpty(); }; return c(err); }; auto onItemHidding = [this, c, onItemRemoval](const QString &err) { if (err.size() > 0) return c(err); // Remove all system values from list deleteListRow(0, LIST_ITEM_REMOVAL_STUB, onItemRemoval); }; int dbRowIndex = i; if (isReverseOrder()) { dbRowIndex = -i - 1; } verifyListItemPosition( dbRowIndex, [this, dbRowIndex, c, onItemHidding](const QString &err) { if (err.size() > 0) return c(err); // Replace value by system string setListRow(dbRowIndex, LIST_ITEM_REMOVAL_STUB, onItemHidding); }); } QList ListKeyModel::getRangeCmd(QVariant rowStartId, unsigned long count) { if (isReverseOrder()) { long rowStart = -rowStartId.toLongLong() - 1; long rowStop = rowStart - count + 1; QList cmd {m_rowsLoadCmd, m_keyFullPath, QString::number(rowStop).toLatin1(), QString::number(rowStart).toLatin1()}; return cmd; } else { return KeyModel::getRangeCmd(rowStartId, count); } } int ListKeyModel::addLoadedRowsToCache(const QVariantList &rows, QVariant rowStartId) { if (isReverseOrder()) { return ListLikeKeyModel::addLoadedRowsToCache( QList(rows.rbegin(), rows.rend()), rowStartId); } else { return ListLikeKeyModel::addLoadedRowsToCache(rows, rowStartId); } } void ListKeyModel::verifyListItemPosition(int row, Callback c) { auto verifyResponse = [this, row](RedisClient::Response r, Callback c) { QVariantList currentState = r.value().toList(); QByteArray cachedValue; if (isReverseOrder()) { cachedValue = m_rowsCache[-row - 1]; } else { cachedValue = m_rowsCache[row]; } bool isChanged = currentState.size() != 1 || currentState[0].toByteArray() != QString(cachedValue); if (isChanged) { return c(QCoreApplication::translate("RESP", "The row has been changed on server." "Reload and try again.")); } else { return c(QString()); } }; executeCmd({"LRANGE", m_keyFullPath, QString::number(row).toLatin1(), QString::number(row).toLatin1()}, c, verifyResponse); } void ListKeyModel::addListRow(const QByteArray &value, Callback c) { executeCmd({"LPUSH", m_keyFullPath, value}, c); } void ListKeyModel::setListRow(int pos, const QByteArray &value, Callback c) { executeCmd({"LSET", m_keyFullPath, QString::number(pos).toLatin1(), value}, c); } void ListKeyModel::deleteListRow(int count, const QByteArray &value, Callback c) { executeCmd({"LREM", m_keyFullPath, QString::number(count).toLatin1(), value}, c); } bool ListKeyModel::isReverseOrder() const { return m_filters.value("order", "default") == "reverse"; } ================================================ FILE: src/app/models/key-models/listkey.h ================================================ #pragma once #include #include "listlikekey.h" class ListKeyModel : public ListLikeKeyModel { public: ListKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl); QString type() override; void addRow(const QVariantMap &, ValueEditor::Model::Callback c) override; virtual void updateRow(int rowIndex, const QVariantMap &, ValueEditor::Model::Callback c) override; void removeRow(int, ValueEditor::Model::Callback c) override; protected: virtual QList getRangeCmd(QVariant rowStartId, unsigned long count) override; int addLoadedRowsToCache(const QVariantList& rows, QVariant rowStart) override; private: void verifyListItemPosition(int row, Callback c); void addListRow(const QByteArray &value, Callback c); void setListRow(int pos, const QByteArray &value, Callback c); void deleteListRow(int count, const QByteArray &value, Callback c); bool isReverseOrder() const; }; ================================================ FILE: src/app/models/key-models/listlikekey.cpp ================================================ #include "listlikekey.h" ListLikeKeyModel::ListLikeKeyModel( QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl, QByteArray rowsCountCmd, QByteArray rowsLoadCmd) : KeyModel(connection, fullPath, dbIndex, ttl, rowsCountCmd, rowsLoadCmd) {} QStringList ListLikeKeyModel::getColumnNames() { return QStringList() << "rowNumber" << "value"; } QHash ListLikeKeyModel::getRoles() { QHash roles; roles[Roles::Value] = "value"; roles[Roles::RowNumber] = "rowNumber"; return roles; } QVariant ListLikeKeyModel::getData(int rowIndex, int dataRole) { if (!isRowLoaded(rowIndex)) return QVariant(); switch (dataRole) { case Value: return m_rowsCache[rowIndex]; case RowNumber: return rowIndex; } return QVariant(); } int ListLikeKeyModel::addLoadedRowsToCache(const QVariantList &rows, QVariant rowStartId) { QList result; auto rowStart = rowStartId.toLongLong(); foreach (QVariant row, rows) { result.push_back(row.toByteArray()); } m_rowsCache.addLoadedRange({rowStart, rowStart + result.size() - 1}, result); return result.size(); } ================================================ FILE: src/app/models/key-models/listlikekey.h ================================================ #pragma once #include "abstractkey.h" class ListLikeKeyModel : public KeyModel { public: ListLikeKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl, QByteArray rowsCountCmd, QByteArray rowsLoadCmd); QStringList getColumnNames() override; QHash getRoles() override; QVariant getData(int rowIndex, int dataRole) override; protected: enum Roles { RowNumber = Qt::UserRole + 1, Value }; protected: int addLoadedRowsToCache(const QVariantList& rows, QVariant rowStart) override; }; ================================================ FILE: src/app/models/key-models/newkeyrequest.cpp ================================================ #include "newkeyrequest.h" NewKeyRequest::NewKeyRequest(QSharedPointer connection, int dbIndex, QSharedPointer callback, QString keyPrefix) : m_connection(connection), m_dbIndex(dbIndex), m_callback(callback), m_keyName(keyPrefix), m_valueFilePath(QString()){ } void NewKeyRequest::loadAdditionalKeyTypesInfo(QJSValue jsCallback) { if (!m_connection) { qWarning() << "Invalid connection"; return; } m_jsCallback = jsCallback; qDebug() << m_jsCallback.isCallable(); m_connection->refreshServerInfo([this](){ if (!m_jsCallback.isCallable()) { qDebug() << "JS callback is not callable"; return; } if (!m_connection) { m_jsCallback.call(QJSValueList{}); return; } auto loadedModules = m_connection->getEnabledModules().keys(); QJSValueList supportedKeyTypesExposedByModules; for (QString mod : loadedModules) { if (mod == "ReJSON") supportedKeyTypesExposedByModules.append(QJSValue(mod)); } m_jsCallback.call(supportedKeyTypesExposedByModules); }); } ================================================ FILE: src/app/models/key-models/newkeyrequest.h ================================================ #pragma once #include #include #include #include #include #include "modules/connections-tree/operations.h" class NewKeyRequest { Q_GADGET Q_PROPERTY(QString dbIdString READ dbIdString) Q_PROPERTY(QString keyName READ keyName WRITE setKeyName) Q_PROPERTY(QString keyType READ keyType WRITE setKeyType) Q_PROPERTY(QVariantMap value READ value WRITE setValue) Q_PROPERTY(QString valueFilePath READ valueFilePath WRITE setValueFilePath) public: NewKeyRequest(QSharedPointer connection, int dbIndex, QSharedPointer callback, QString keyPrefix = QString()); NewKeyRequest() {} QString dbIdString() { return QString("%1:db%2") .arg(m_connection->getConfig().name()) .arg(m_dbIndex); } int dbIndex() { return m_dbIndex; } QString keyName() { return m_keyName; } void setKeyName(QString k) { m_keyName = k; } QString keyType() { return m_keyType; } void setKeyType(QString k) { m_keyType = k; } QVariantMap value() const { return m_value; } void setValue(const QVariantMap& v) { m_value = v; } QString valueFilePath() const { return m_valueFilePath; } void setValueFilePath(const QString& path) { m_valueFilePath = path; } QSharedPointer connection() { return m_connection; } void callback() const { if (m_callback) m_callback->call(); } Q_INVOKABLE void loadAdditionalKeyTypesInfo(QJSValue jsCallback); private: QSharedPointer m_connection = nullptr; int m_dbIndex = -1; QSharedPointer m_callback; QJSValue m_jsCallback; QString m_keyName; QString m_keyType; QVariantMap m_value; QString m_valueFilePath; }; Q_DECLARE_METATYPE(NewKeyRequest) ================================================ FILE: src/app/models/key-models/rejsonkey.cpp ================================================ #include "rejsonkey.h" #include ReJSONKeyModel::ReJSONKeyModel( QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl) : KeyModel(connection, fullPath, dbIndex, ttl) {} QString ReJSONKeyModel::type() { return "ReJSON"; } QStringList ReJSONKeyModel::getColumnNames() { return QStringList() << "value"; } QHash ReJSONKeyModel::getRoles() { QHash roles; roles[Roles::Value] = "value"; return roles; } QVariant ReJSONKeyModel::getData(int rowIndex, int dataRole) { if (!isRowLoaded(rowIndex)) return QVariant(); if (dataRole == Roles::Value) return m_rowsCache[rowIndex]; return QVariant(); } void ReJSONKeyModel::updateRow(int rowIndex, const QVariantMap& row, Callback c) { if (rowIndex > 0 || !isRowValid(row)) { qDebug() << "Row is not valid"; return; } QByteArray value = row.value("value").toByteArray(); if (value.isEmpty()) return; auto responseHandler = [this, value](RedisClient::Response r, Callback c) { if (r.isOkMessage()) { m_rowsCache.clear(); m_rowsCache.addLoadedRange({0, 0}, (QList() << value)); return c(QString()); } else { return c(r.value().toString()); } }; executeCmd({"JSON.SET", m_keyFullPath, ".", value}, c, responseHandler); } void ReJSONKeyModel::addRow(const QVariantMap& row, Callback c) { updateRow(0, row, c); } void ReJSONKeyModel::loadRows(QVariant, unsigned long, LoadRowsCallback callback) { auto onConnectionError = [callback](const QString& err) { return callback(err, 0); }; auto responseHandler = [this, callback](RedisClient::Response r, Callback) { m_rowsCache.clear(); m_rowsCache.push_back(r.value().toByteArray()); callback(QString(), 1); }; executeCmd({"JSON.GET", m_keyFullPath, "noescape"}, onConnectionError, responseHandler, RedisClient::Response::String); } void ReJSONKeyModel::removeRow(int, Callback) { m_rowCount--; setRemovedIfEmpty(); } ================================================ FILE: src/app/models/key-models/rejsonkey.h ================================================ #pragma once #include "abstractkey.h" class ReJSONKeyModel : public KeyModel { public: ReJSONKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl); QString type() override; QStringList getColumnNames() override; QHash getRoles() override; QVariant getData(int rowIndex, int dataRole) override; void addRow(const QVariantMap&, ValueEditor::Model::Callback c) override; virtual void updateRow(int rowIndex, const QVariantMap& row, ValueEditor::Model::Callback c) override; void loadRows(QVariant, unsigned long, LoadRowsCallback callback) override; void removeRow(int, ValueEditor::Model::Callback c) override; protected: int addLoadedRowsToCache(const QVariantList&, QVariant) override { return 1; } private: enum Roles { Value = Qt::UserRole + 1 }; }; ================================================ FILE: src/app/models/key-models/rowcache.h ================================================ #pragma once #include #include #include #include typedef qlonglong RowIndex; class CacheRange : public QPair { public: CacheRange(const RowIndex& f = -1, const RowIndex& s = -1) : QPair(f, s) {} bool isEmpty() const { return first == -1 && second == -1; } }; template class MappedCache { public: MappedCache() : m_valid(false) {} bool isValid() const { return m_valid; } void addLoadedRange(const CacheRange& range, const QList& dataForRange) { if (!isValid()) clear(); m_mapping[range] = dataForRange; } bool isRowLoaded(RowIndex index) { CacheRange i = findTargetRange(index); return !i.isEmpty(); } T getRow(RowIndex index) { if (!isRowLoaded(index)) return T(); CacheRange i = findTargetRange(index); return m_mapping[i].at(index - i.first); } T operator[](RowIndex index) { return getRow(index); } void replace(RowIndex index, T row) { if (!isRowLoaded(index)) { throw std::out_of_range("Invalid row"); } CacheRange i = findTargetRange(index); return m_mapping[i].replace(index - i.first, row); } void removeAt(RowIndex index) { if (!isRowLoaded(index)) { throw std::out_of_range("Invalid row"); } CacheRange i = findTargetRange(index); m_mapping[i].removeAt(index - i.first); CacheRange newKey{i.first, i.second - 1}; replaceRangeInMapping(newKey, i); m_valid = false; } void push_back(const T& row) { CacheRange newKey{0, 1}; if (m_mapping.size() > 0) { newKey.first += m_mapping.lastKey().first; newKey.second += m_mapping.lastKey().second; m_mapping.last().push_back(row); replaceRangeInMapping(newKey); } else { m_mapping.insert(newKey, QList{row}); } } unsigned long long size() const { unsigned long long cacheSize = 0; for (auto cachePage : m_mapping) { cacheSize += cachePage.size(); } return cacheSize; } void clear() { m_mapping.clear(); m_valid = true; } private: CacheRange findTargetRange(RowIndex index) { for (auto i = m_mapping.constBegin(); i != m_mapping.constEnd(); ++i) { if (i.key().first <= index && index <= i.key().second) { return i.key(); } } return CacheRange(); } void replaceRangeInMapping(const CacheRange& newRange, const CacheRange& current = CacheRange()) { CacheRange replaceKey = current.isEmpty() ? m_mapping.lastKey() : current; m_mapping[newRange] = m_mapping[replaceKey]; m_mapping.remove(replaceKey); } private: QMap> m_mapping; bool m_valid; }; ================================================ FILE: src/app/models/key-models/setkey.cpp ================================================ #include "setkey.h" #include SetKeyModel::SetKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl) : ListLikeKeyModel(connection, fullPath, dbIndex, ttl, "SCARD", "SSCAN") {} QString SetKeyModel::type() { return "set"; } void SetKeyModel::updateRow(int rowIndex, const QVariantMap &row, Callback c) { if (!isRowLoaded(rowIndex) || !isRowValid(row)) { emit m_notifier->error(QCoreApplication::translate("RESP", "Invalid row")); return; } QByteArray cachedRow = m_rowsCache[rowIndex]; QByteArray newRow(row["value"].toByteArray()); auto onRowAdded = [this, c, rowIndex, newRow](const QString &err) { if (err.isEmpty()) m_rowsCache.replace(rowIndex, newRow); return c(err); }; deleteSetRow(cachedRow, [this, c, newRow, onRowAdded](const QString &err) { if (err.size() > 0) return c(err); addSetRow(newRow, onRowAdded); }); } void SetKeyModel::addRow(const QVariantMap &row, Callback c) { if (!isRowValid(row)) { return c(QCoreApplication::translate("RESP", "Invalid row")); } addSetRow(row["value"].toByteArray(), [this, c](const QString &err) { if (err.isEmpty()) { m_rowCount++; } return c(err); }); } void SetKeyModel::removeRow(int i, Callback c) { if (!isRowLoaded(i)) return; QByteArray value = m_rowsCache[i]; deleteSetRow(value, [this, c, i](const QString &err) { if (err.isEmpty()) { m_rowCount--; m_rowsCache.removeAt(i); setRemovedIfEmpty(); } return c(err); }); } void SetKeyModel::addSetRow(const QByteArray &value, Callback c) { executeCmd({"SADD", m_keyFullPath, value}, c); } void SetKeyModel::deleteSetRow(const QByteArray &value, Callback c) { executeCmd({"SREM", m_keyFullPath, value}, c); } ================================================ FILE: src/app/models/key-models/setkey.h ================================================ #pragma once #include "listlikekey.h" class SetKeyModel : public ListLikeKeyModel { public: SetKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl); QString type() override; void addRow(const QVariantMap &, Callback c) override; virtual void updateRow(int rowIndex, const QVariantMap &, Callback c) override; void removeRow(int, Callback c) override; private: void addSetRow(const QByteArray &value, Callback c); void deleteSetRow(const QByteArray &value, Callback c); }; ================================================ FILE: src/app/models/key-models/sortedsetkey.cpp ================================================ #include "sortedsetkey.h" #include SortedSetKeyModel::SortedSetKeyModel( QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl) : KeyModel(connection, fullPath, dbIndex, ttl, "ZCARD", "ZRANGE WITHSCORES") {} QString SortedSetKeyModel::type() { return "zset"; } QStringList SortedSetKeyModel::getColumnNames() { return QStringList() << "rowNumber" << "value" << "score"; } QHash SortedSetKeyModel::getRoles() { QHash roles; roles[Roles::RowNumber] = "rowNumber"; roles[Roles::Value] = "value"; roles[Roles::Score] = "score"; return roles; } QVariant SortedSetKeyModel::getData(int rowIndex, int dataRole) { if (!isRowLoaded(rowIndex)) return QVariant(); QPair row = m_rowsCache[rowIndex]; if (dataRole == Roles::Value) return row.first; else if (dataRole == Roles::Score) return row.second.toDouble(); else if (dataRole == Roles::RowNumber) return rowIndex; return QVariant(); } void SortedSetKeyModel::updateRow(int rowIndex, const QVariantMap &row, Callback c) { if (!isRowLoaded(rowIndex) || !isRowValid(row)) { emit m_notifier->error(QCoreApplication::translate("RESP", "Invalid row")); return; } QPair cachedRow = m_rowsCache[rowIndex]; bool valueChanged = cachedRow.first != row["value"].toByteArray(); bool scoreChanged = cachedRow.second != row["score"].toByteArray(); QPair newRow( (valueChanged) ? row["value"].toByteArray() : cachedRow.first, (scoreChanged) ? row["score"].toByteArray() : cachedRow.second); auto onRowAdded = [this, c, rowIndex, newRow](const QString &err) { if (err.isEmpty()) m_rowsCache.replace(rowIndex, newRow); return c(err); }; if (valueChanged) { deleteSortedSetRow( cachedRow.first, [this, c, onRowAdded, newRow](const QString &err) { if (err.size() > 0) return c(err); addSortedSetRow(newRow.first, newRow.second, onRowAdded, false); }); } else { addSortedSetRow(newRow.first, newRow.second, onRowAdded, true); } } void SortedSetKeyModel::addRow(const QVariantMap &row, Callback c) { if (!isRowValid(row)) { return c(QCoreApplication::translate("RESP", "Invalid row")); } auto onAdded = [this, c](const QString &err) { if (err.isEmpty()) m_rowCount++; return c(err); }; addSortedSetRow(row["value"].toByteArray(), row["score"].toByteArray(), onAdded); } void SortedSetKeyModel::removeRow(int i, Callback c) { if (!isRowLoaded(i)) return; QByteArray value = m_rowsCache[i].first; executeCmd({"ZREM", m_keyFullPath, value}, [this, c, i](const QString &err) { if (err.isEmpty()) { m_rowCount--; m_rowsCache.removeAt(i); setRemovedIfEmpty(); } return c(err); }); } void SortedSetKeyModel::addSortedSetRow(const QByteArray &value, QByteArray score, Callback c, bool updateExisting) { QList cmd; if (updateExisting) { cmd = {"ZADD", m_keyFullPath, "XX", score, value}; } else { cmd = {"ZADD", m_keyFullPath, score, value}; } executeCmd(cmd, c, CmdHandler(), RedisClient::Response::Integer); } void SortedSetKeyModel::deleteSortedSetRow(const QByteArray &value, Callback c) { executeCmd({"ZREM", m_keyFullPath, value}, c); } int SortedSetKeyModel::addLoadedRowsToCache(const QVariantList &rows, QVariant rowStartId) { QList> result; for (QVariantList::const_iterator item = rows.begin(); item != rows.end(); ++item) { QPair value; value.first = item->toByteArray(); ++item; if (item == rows.end()) { emit m_notifier->error(QCoreApplication::translate( "RESP", "Data was loaded from server partially.")); return 0; } value.second = item->toByteArray(); result.push_back(value); } auto rowStart = rowStartId.toLongLong(); m_rowsCache.addLoadedRange({rowStart, rowStart + result.size() - 1}, result); return result.size(); } ================================================ FILE: src/app/models/key-models/sortedsetkey.h ================================================ #pragma once #include "abstractkey.h" class SortedSetKeyModel : public KeyModel> { public: SortedSetKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl); QString type() override; QStringList getColumnNames() override; QHash getRoles() override; QVariant getData(int rowIndex, int dataRole) override; void addRow(const QVariantMap&, Callback c) override; virtual void updateRow(int rowIndex, const QVariantMap&, Callback c) override; void removeRow(int, Callback c) override; protected: int addLoadedRowsToCache(const QVariantList& list, QVariant rowStart) override; private: enum Roles { RowNumber = Qt::UserRole + 1, Value, Score }; void addSortedSetRow(const QByteArray& value, QByteArray score, Callback c, bool updateExisting = false); void deleteSortedSetRow(const QByteArray& value, Callback c); }; ================================================ FILE: src/app/models/key-models/stream.cpp ================================================ #include "stream.h" #include #include #include #include #include "app/jsonutils.h" StreamKeyModel::StreamKeyModel( QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl) : KeyModel(connection, fullPath, dbIndex, ttl, "XLEN", QByteArray()) {} QString StreamKeyModel::type() { return "stream"; } QStringList StreamKeyModel::getColumnNames() { return QStringList() << "rowNumber" << "id" << "value"; } QHash StreamKeyModel::getRoles() { QHash roles; roles[Roles::RowNumber] = "rowNumber"; roles[Roles::ID] = "id"; roles[Roles::Value] = "value"; return roles; } QVariant StreamKeyModel::getData(int rowIndex, int dataRole) { if (!isRowLoaded(rowIndex)) return QVariant(); switch (dataRole) { case Value: return QJsonDocument::fromVariant(m_rowsCache[rowIndex].second) .toJson(QJsonDocument::Compact); case ID: return m_rowsCache[rowIndex].first; case RowNumber: return rowIndex; } return QVariant(); } void StreamKeyModel::addRow(const QVariantMap &row, ValueEditor::Model::Callback c) { if (!isRowValid(row)) { c(QCoreApplication::translate("RESP", "Invalid row")); return; } QList cmd = {"XADD", m_keyFullPath, row["id"].toByteArray()}; QJsonParseError err; QJsonDocument jsonValues = QJsonDocument::fromJson(row["value"].toByteArray(), &err); if (err.error != QJsonParseError::NoError || !jsonValues.isObject()) { return c(QCoreApplication::translate("RESP", "Invalid row")); } auto valuesObject = jsonValues.object(); for (auto key : valuesObject.keys()) { cmd.append(key.toUtf8()); if (valuesObject[key].isArray()) { QJsonDocument d = QJsonDocument(valuesObject[key].toArray()); cmd.append(d.toJson(QJsonDocument::Compact)); } else if (valuesObject[key].isObject()) { QJsonDocument d = QJsonDocument(valuesObject[key].toObject()); cmd.append(d.toJson(QJsonDocument::Compact)); } else { cmd.append(valuesObject[key].toVariant().toByteArray()); } } executeCmd(cmd, c); } void StreamKeyModel::updateRow(int, const QVariantMap &, ValueEditor::Model::Callback) { //NOTE(u_glide): Redis Streams doesn't support editing (yet?) } void StreamKeyModel::removeRow(int i, ValueEditor::Model::Callback c) { if (!isRowLoaded(i)) return; executeCmd({"XDEL", m_keyFullPath, m_rowsCache[i].first}, c); } void StreamKeyModel::loadRowsCount(ValueEditor::Model::Callback c) { executeCmd( {"XINFO", "STREAM", m_keyFullPath}, c, [this](RedisClient::Response r, Callback c) { auto info = r.value().toList(); auto it = info.begin(); while (it != info.end()) { if (!it->canConvert(QMetaType::QByteArray)) { continue; } QByteArray propertyName = it->toByteArray(); it++; if (it == info.end()) break; if (propertyName == QByteArray("length")) { m_rowCount = it->toLongLong(); } else if (propertyName == QByteArray("first-entry") || propertyName == QByteArray("last-entry")) { auto list = it->toList(); if (list.size() > 0) { m_filters[QString::fromLatin1(propertyName)] = list[0]; } } it++; } c(QString()); }, RedisClient::Response::Type::Array); } int StreamKeyModel::addLoadedRowsToCache(const QVariantList &rows, QVariant rowStartId) { QList> result; for (QVariantList::const_iterator item = rows.begin(); item != rows.end(); ++item) { QPair value; auto rowValues = item->toList(); value.first = rowValues[0].toByteArray(); QVariantList valuesList = rowValues[1].toList(); QVariantMap mappedVal; for (QVariantList::const_iterator valItem = valuesList.begin(); valItem != valuesList.end(); ++valItem) { auto valKey = valItem->toByteArray(); valItem++; QByteArray fieldValue = valItem->toByteArray(); if (JSONUtils::isJSON(fieldValue)) { auto doc = QJsonDocument::fromJson(fieldValue); if (doc.isObject() || doc.isArray()) { mappedVal[valKey] = doc.toVariant(); } else { mappedVal[valKey] = fieldValue; } } else { mappedVal[valKey] = fieldValue; } } value.second = mappedVal; result.push_back(value); } auto rowStart = rowStartId.toLongLong(); m_rowsCache.addLoadedRange({rowStart, rowStart + result.size() - 1}, result); return result.size(); } QList StreamKeyModel::getRangeCmd(QVariant rowStartId, unsigned long count) { QList cmd; cmd << "XREVRANGE" << m_keyFullPath; if (filter("end").isNull()) { // end cmd << "+"; } else { cmd << QString::number(filter("end").toLongLong()).toLatin1(); } if (filter("start").isNull()) { // start unsigned long rowStart = rowStartId.toULongLong(); if (isRowLoaded(rowStart - 1)) { cmd << m_rowsCache[rowStart - 1].first; } else { cmd << "-"; } } else { cmd << QString::number(filter("start").toLongLong()).toLatin1(); } return cmd << "COUNT" << QString::number(count).toLatin1(); } ================================================ FILE: src/app/models/key-models/stream.h ================================================ #pragma once #include "abstractkey.h" class StreamKeyModel : public KeyModel> { public: StreamKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl); QString type() override; QStringList getColumnNames() override; QHash getRoles() override; QVariant getData(int rowIndex, int dataRole) override; void addRow(const QVariantMap &, Callback) override; virtual void updateRow(int rowIndex, const QVariantMap &, Callback) override; void removeRow(int, Callback) override; void loadRowsCount(ValueEditor::Model::Callback c) override; protected: int addLoadedRowsToCache(const QVariantList &list, QVariant rowStart) override; virtual QList getRangeCmd(QVariant rowStartId, unsigned long count) override; protected: enum Roles { RowNumber = Qt::UserRole + 1, ID, Value }; }; ================================================ FILE: src/app/models/key-models/stringkey.cpp ================================================ #include "stringkey.h" #include StringKeyModel::StringKeyModel( QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl) : KeyModel(connection, fullPath, dbIndex, ttl), m_type("string") {} QString StringKeyModel::type() { return m_type; } QStringList StringKeyModel::getColumnNames() { return QStringList() << "value"; } QHash StringKeyModel::getRoles() { QHash roles; roles[Roles::Value] = "value"; return roles; } QVariant StringKeyModel::getData(int rowIndex, int dataRole) { if (rowIndex > 0 || !isRowLoaded(rowIndex)) return QVariant(); if (dataRole == Roles::Value) return m_rowsCache[rowIndex]; return QVariant(); } void StringKeyModel::updateRow(int rowIndex, const QVariantMap& row, Callback c) { if (rowIndex > 0 || !isRowValid(row)) { qDebug() << "Row is not valid"; return; } QByteArray value = row.value("value").toByteArray(); executeCmd( {"SET", m_keyFullPath, value}, [this, c, value](const QString& err) { if (err.isEmpty()) { m_rowsCache.clear(); m_rowsCache.addLoadedRange({0, 0}, (QList() << value)); } return c(err); }); } void StringKeyModel::addRow(const QVariantMap& row, Callback c) { if (m_type == "hyperloglog") { QByteArray value = row.value("value").toByteArray(); executeCmd( {"PFADD", m_keyFullPath, value}, [this, c](const QString& err) { m_rowCount++; return c(err); }); } else { updateRow(0, row, c); } } void StringKeyModel::loadRows(QVariant, unsigned long, LoadRowsCallback callback) { auto onConnectionError = [callback](const QString& err) { return callback(err, 0); }; auto responseHandler = [this, callback](RedisClient::Response r, Callback) { m_rowsCache.clear(); QByteArray value = r.value().toByteArray(); m_rowsCache.push_back(value); m_rowCount = 1; // Detect HyperLogLog if (value.startsWith("HYLL")) { executeCmd( {"PFCOUNT", m_keyFullPath}, [callback](const QString&) { callback(QString(), 1); }, [this, callback](RedisClient::Response r, Callback) { m_type = "hyperloglog"; m_rowCount = r.value().toUInt(); callback(QString(), m_rowCount); }, RedisClient::Response::Integer); } else { callback(QString(), 1); } }; executeCmd({"GET", m_keyFullPath}, onConnectionError, responseHandler, RedisClient::Response::String); } void StringKeyModel::removeRow(int, Callback) { m_rowCount--; setRemovedIfEmpty(); } ================================================ FILE: src/app/models/key-models/stringkey.h ================================================ #pragma once #include "abstractkey.h" class StringKeyModel : public KeyModel { public: StringKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl); QString type() override; QStringList getColumnNames() override; QHash getRoles() override; QVariant getData(int rowIndex, int dataRole) override; void addRow(const QVariantMap&, Callback c) override; virtual void updateRow(int rowIndex, const QVariantMap& row, Callback c) override; void loadRows(QVariant, unsigned long, LoadRowsCallback callback) override; void removeRow(int, Callback c) override; virtual unsigned long rowsCount() override { return m_rowCount; } protected: int addLoadedRowsToCache(const QVariantList&, QVariant) override { return 1; } private: enum Roles { Value = Qt::UserRole + 1 }; QString m_type; }; ================================================ FILE: src/app/models/key-models/unknownkey.cpp ================================================ #include "unknownkey.h" #include UnknownKeyModel::UnknownKeyModel( QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl, QString type) : KeyModel(connection, fullPath, dbIndex, ttl), m_type(type) {} ================================================ FILE: src/app/models/key-models/unknownkey.h ================================================ #pragma once #include "abstractkey.h" #define EMPTY_METHOD override {qWarning() << "Operation is not supported";} class UnknownKeyModel : public KeyModel { public: UnknownKeyModel(QSharedPointer connection, QByteArray fullPath, int dbIndex, long long ttl, QString type); QString type() override { return m_type; } QStringList getColumnNames() override { return QStringList() << ""; } QHash getRoles() override { return QHash(); } QVariant getData(int, int) override {return QVariant();} void addRow(const QVariantMap&, Callback) EMPTY_METHOD virtual void updateRow(int, const QVariantMap&, Callback) EMPTY_METHOD void loadRows(QVariant, unsigned long, LoadRowsCallback callback) override { return callback("unknown-data-type", 0); } void removeRow(int, Callback) EMPTY_METHOD virtual unsigned long rowsCount() override { return 0; } protected: int addLoadedRowsToCache(const QVariantList&, QVariant) override { return 1; } private: enum Roles { Value = Qt::UserRole + 1 }; QString m_type; }; ================================================ FILE: src/app/models/treeoperations.cpp ================================================ #include "treeoperations.h" #include #include #include #include #include #include #include #include #include "app/events.h" #include "connections-tree/items/serveritem.h" #include "connections-tree/items/databaseitem.h" #include "connections-tree/items/namespaceitem.h" #include "connections-tree/keysrendering.h" TreeOperations::TreeOperations(const ServerConfig &config, QSharedPointer events) : m_events(events), m_dbCount(0), m_connectionMode(RedisClient::Connection::Mode::Normal), m_config(config){ m_connection = QSharedPointer( new RedisClient::Connection(config)); m_events->registerLoggerForConnection(*m_connection); } void TreeOperations::loadDatabases( QSharedPointer c, QSharedPointer> d, std::function callback) { if (!d) return; auto connection = c->clone(false); auto connectionWRef = connection.toWeakRef(); d->onCanceled([connectionWRef]() { QtConcurrent::run([connectionWRef]() { auto connection = connectionWRef.toStrongRef(); if (connection) connection->disconnect(); }); }); m_events->registerLoggerForConnection(*connection); if (!connect(connection)) { return callback(RedisClient::DatabaseList(), QString("Cannot connect to redis-server")); } if (d && d->future().isCanceled()) { return; } RedisClient::DatabaseList availableDatabeses = connection->getKeyspaceInfo(); if (connection->mode() == RedisClient::Connection::Mode::Cluster) { return callback(availableDatabeses, QString()); } // detect all databases RedisClient::Response scanningResp; int lastDbIndex = (availableDatabeses.size() == 0) ? 0 : availableDatabeses.lastKey() + 1; if (m_dbCount > 0) { for (int index = lastDbIndex; index < m_dbCount; index++) { availableDatabeses.insert(index, 0); } return callback(availableDatabeses, QString()); } else { m_dbCount = lastDbIndex; auto collectedDatabases = QSharedPointer( new RedisClient::DatabaseList(availableDatabeses)); recursiveSelectScan(d, connection, collectedDatabases, callback); } } void TreeOperations::recursiveSelectScan( QSharedPointer> d, QSharedPointer c, QSharedPointer dbList, std::function callback) { if (d && d->future().isCanceled()) { return; } if (m_dbCount >= m_config.databaseScanLimit() || !c) { return callback(*dbList, QString()); } auto errHandler = [callback, dbList](const QString& err) { if (dbList && dbList->size() > 0) { callback(*dbList, QString()); } else { callback(RedisClient::DatabaseList(), err); } }; c->cmd( {"select", QString::number(m_dbCount).toLatin1()}, this, -1, [this, dbList, c, callback, d](const RedisClient::Response& scanningResp) { if (d && d->future().isCanceled()) { return; } if (!scanningResp.isOkMessage()) { callback(*dbList, QString()); return; } dbList->insert(m_dbCount, 0); m_dbCount++; recursiveSelectScan(d, c, dbList, callback); }, errHandler); } bool TreeOperations::connect(QSharedPointer c) { if (c->isConnected()) return true; try { if (!c->connect(true)) { emit m_events->error( QCoreApplication::translate( "RESP", "Cannot connect to server '%1'. Check log for details.") .arg(m_connection->getConfig().name())); return false; } m_connectionMode = c->mode(); return true; } catch (const RedisClient::Connection::SSHSupportException& e) { emit m_events->error( QCoreApplication::translate("RESP", "Open Source version of RESP.app doesn't support SSH tunneling.

" "To get fully-featured application, please buy subscription on " "resp.app.

" "Every single subscription gives us funds to continue " "the development process and provide support to our users.
" "If you have any questions please feel free to contact us " "at support@resp.app " "or join Telegram chat.") ); return false; } catch (const RedisClient::Connection::Exception& e) { emit m_events->error( QCoreApplication::translate("RESP", "Connection error: ") + QString(e.what())); return false; } } void TreeOperations::requestBulkOperation( ConnectionsTree::AbstractNamespaceItem& ns, BulkOperations::Manager::Operation op, BulkOperations::AbstractOperation::OperationCallback callback) { QString pattern = QString("%1%2*") .arg(QString::fromUtf8(ns.getFullPath())) .arg(ns.getFullPath().size() > 0 ? m_config.namespaceSeparator() : ""); QRegExp filter(pattern, Qt::CaseSensitive, QRegExp::Wildcard); auto dbIndex = ns.getDbIndex(); getReadyConnection([this, dbIndex, filter, op, callback](QSharedPointer c) { // NOTE(u_glide): Use "clean" connection wihout logger here for better // performance emit m_events->requestBulkOperation(c->clone(), dbIndex, op, filter, callback); }); } void TreeOperations::getReadyConnection(TreeOperations::PendingOperation callback) { if (m_config.askForSshPassword() && m_config.sshPassword().isEmpty()) { m_pendingOperation = callback; m_config.setOwner(sharedFromThis().toWeakRef()); emit secretRequired(m_config, ServerConfig::SSH_SECRET_ID); } else { return callback(m_connection); } } QFuture TreeOperations::getDatabases( QSharedPointer callback) { m_dbScanOp = QSharedPointer>( new AsyncFuture::Deferred()); getReadyConnection( [this, callback](QSharedPointer c) { QtConcurrent::run(this, &TreeOperations::loadDatabases, c, m_dbScanOp, [callback](RedisClient::DatabaseList dbs, const QString& err){ callback->call(dbs, err); }); }); return m_dbScanOp->future(); } void TreeOperations::loadNamespaceItems( uint dbIndex, const QString& filter, QSharedPointer callback) { QString keyPattern = filter.isEmpty() ? m_config.keysPattern() : filter; if (m_filterHistory.contains(keyPattern)) { m_filterHistory[keyPattern] = m_filterHistory[keyPattern].toInt() + 1; } else { m_filterHistory[keyPattern] = 1; } m_config.setFilterHistory(m_filterHistory); emit filterHistoryUpdated(); QSettings settings; qlonglong scanLimit = settings.value("app/scanLimit", DEFAULT_SCAN_LIMIT).toLongLong(); getReadyConnection([this, dbIndex, filter, callback, keyPattern, scanLimit](QSharedPointer c) { if (!connect(c)) return; auto processErr = [callback](const QString& err) { return callback->call( RedisClient::Connection::RawKeysList(), QCoreApplication::translate("RESP", "Cannot load keys: %1").arg(err)); }; auto callbackWrapper = [callback](const RedisClient::Connection::RawKeysList &keys, const QString &err) { return callback->call(keys, err); }; try { if (m_connection->mode() == RedisClient::Connection::Mode::Cluster) { m_connection->getClusterKeys(callbackWrapper, keyPattern, scanLimit); } else { m_connection->cmd( {"ping"}, this, dbIndex, [this, callbackWrapper, keyPattern, processErr, scanLimit](const RedisClient::Response& r) { if (r.isErrorMessage()) { return processErr(r.value().toString()); } m_connection->getDatabaseKeys(callbackWrapper, keyPattern, -1, scanLimit); }, [processErr](const QString& err) { return processErr(err); }); } } catch (const RedisClient::Connection::Exception& error) { processErr(error.what()); } }); } void TreeOperations::disconnect() { m_connection->disconnect(); } void TreeOperations::resetConnection() { auto oldConnection = m_connection; setConnection(oldConnection->clone()); QtConcurrent::run([oldConnection]() { oldConnection->disconnect(); }); } QString TreeOperations::getNamespaceSeparator() { return m_config.namespaceSeparator(); } QString TreeOperations::defaultFilter() { return m_config.keysPattern(); } QVariantMap TreeOperations::getFilterHistory() { m_filterHistory = m_config.filterHistory(); return m_filterHistory; } QString TreeOperations::connectionName() const { return m_config.name(); } void TreeOperations::openKeyTab(QSharedPointer key, bool openInNewTab) { getReadyConnection( [this, key, openInNewTab](QSharedPointer c) { emit m_events->openValueTab(c, key, openInNewTab); }); } void TreeOperations::openConsoleTab(int dbIndex) { getReadyConnection( [this, dbIndex](QSharedPointer c) { emit m_events->openConsole(c, dbIndex, true); }); } void TreeOperations::openNewKeyDialog(int dbIndex, QSharedPointer callback, QString keyPrefix) { getReadyConnection([this, dbIndex, callback, keyPrefix](QSharedPointer c) { emit m_events->newKeyDialog(c, callback, dbIndex, keyPrefix); }); } void TreeOperations::openServerStats() { getReadyConnection([this](QSharedPointer c) { emit m_events->openServerStats(c); }); } void TreeOperations::duplicateConnection() { emit createNewConnection(m_config); } void TreeOperations::notifyDbWasUnloaded(int dbIndex) { emit m_events->closeDbKeys(m_connection, dbIndex); } void TreeOperations::deleteDbKey(ConnectionsTree::KeyItem& key, QSharedPointer callback) { getReadyConnection( [this, &key, callback](QSharedPointer c) { c->cmd( {"DEL", key.getFullPath()}, this, key.getDbIndex(), [this, &key](RedisClient::Response) { key.setRemoved(); QRegExp filter(key.getFullPath(), Qt::CaseSensitive, QRegExp::Wildcard); if (m_events) m_events->closeDbKeys(m_connection, key.getDbIndex(), filter); }, [this, callback](const QString& err) { QString errorMsg = QCoreApplication::translate("RESP", "Delete key error: %1") .arg(err); callback->call(errorMsg); if (m_events) m_events->error(errorMsg); }); }); } void TreeOperations::deleteDbKeys(ConnectionsTree::DatabaseItem& db) { auto self = sharedFromThis().toWeakRef(); requestBulkOperation( db, BulkOperations::Manager::Operation::DELETE_KEYS, [self, this, &db](QRegExp filter, int, const QStringList&) { if (!self) { return; } uint dbIndex = db.getDbIndex(); db.reload(); if (m_events && m_connection) { getReadyConnection( [this, dbIndex, filter](QSharedPointer c) { emit m_events->closeDbKeys(c, dbIndex, filter); }); } }); } void TreeOperations::deleteDbNamespace(ConnectionsTree::NamespaceItem& ns) { auto self = sharedFromThis().toWeakRef(); requestBulkOperation( ns, BulkOperations::Manager::Operation::DELETE_KEYS, [this, self, &ns](QRegExp filter, int, const QStringList&) { if (!self) { return; } uint dbIndex = ns.getDbIndex(); ns.setRemoved(); if (m_events && m_connection) { getReadyConnection( [this, dbIndex, filter](QSharedPointer c) { emit m_events->closeDbKeys(c, dbIndex, filter); }); } }); } void TreeOperations::setTTL(ConnectionsTree::AbstractNamespaceItem& ns) { requestBulkOperation(ns, BulkOperations::Manager::Operation::TTL, [](QRegExp, int, const QStringList&) {}); } void TreeOperations::copyKeys(ConnectionsTree::AbstractNamespaceItem& ns) { requestBulkOperation(ns, BulkOperations::Manager::Operation::COPY_KEYS, [](QRegExp, int, const QStringList&) {}); } void TreeOperations::importKeysFromRdb(ConnectionsTree::DatabaseItem& db) { getReadyConnection([this, &db](QSharedPointer c) { emit m_events->requestBulkOperation( c->clone(), db.getDbIndex(), BulkOperations::Manager::Operation::IMPORT_RDB_KEYS, QRegExp(".*"), [&db](QRegExp, int, const QStringList&) { db.reload(); }); }); } void TreeOperations::flushDb(int dbIndex, QSharedPointer callback) { auto callbackWrapper = [callback](const QString &err) { callback->call(err); }; getReadyConnection( [dbIndex, callbackWrapper](QSharedPointer c) { try { c->flushDbKeys(dbIndex, callbackWrapper); } catch (const RedisClient::Connection::Exception& e) { throw ConnectionsTree::Operations::Exception( QCoreApplication::translate("RESP", "Cannot flush database: ") + QString(e.what())); } }); } QFuture TreeOperations::connectionSupportsMemoryOperations() { return m_connection->isCommandSupported({"MEMORY", "HELP"}); } void TreeOperations::openKeyIfExists(const QByteArray& fullPath, QSharedPointer parent, QSharedPointer callback) { if (!parent) { qWarning() << "TreeOperations::openKeyIfExists > Invalid parent"; return; } getReadyConnection([this, fullPath, parent, callback](QSharedPointer c) { c->cmd( {"exists", fullPath}, this, static_cast(parent->getDbIndex()), [this, parent, fullPath, callback](RedisClient::Response r) { QVariant result = r.value(); if (result.toByteArray() == "1") { auto key = QSharedPointer( new ConnectionsTree::KeyItem(fullPath, parent.toWeakRef(), parent->model(), parent->keysShortNameRendering())); emit m_events->openValueTab(m_connection, key, true); callback->call(QString(), true); } else { callback->call(QString(), false); } }, [callback](const QString& err) { callback->call(err, false); }); }); } void TreeOperations::getUsedMemory(const QList& keys, int dbIndex, QSharedPointer result, QSharedPointer progress) { QList> commands; for (int index = 0; index < keys.size(); ++index) { commands.append({"MEMORY", "USAGE", keys[index]}); } int expectedResponses = commands.size(); auto processedResponses = QSharedPointer(new int(0)); auto totalMemory = QSharedPointer(new qlonglong(0)); m_connection->pipelinedCmd( commands, this, dbIndex, [this, expectedResponses, processedResponses, totalMemory, progress, result](RedisClient::Response r, QString err) { if (!err.isEmpty()) { QString errorMsg = QCoreApplication::translate( "RESP", "Cannot determine amount of used memory by key: %1") .arg(err); m_events->error(errorMsg); } else { QVariant incrResult = r.value(); if (incrResult.canConvert(QVariant::LongLong)) { (*totalMemory) += incrResult.toLongLong(); (*processedResponses)++; } else if (incrResult.canConvert(QVariant::List)) { auto responses = incrResult.toList(); for (auto resp : responses) { (*totalMemory) += resp.toLongLong(); (*processedResponses)++; } } if (progress) progress->call(*totalMemory); if ((*processedResponses) >= expectedResponses && result) { result->call(*totalMemory); } } }, true); } QString TreeOperations::mode() { if (m_connectionMode == RedisClient::Connection::Mode::Cluster) { return QString("cluster"); } else if (m_connectionMode == RedisClient::Connection::Mode::Sentinel) { return QString("sentinel"); } else { return QString("standalone"); } } bool TreeOperations::isConnected() const { return m_connection->isConnected(); } QSharedPointer TreeOperations::connection() { return m_connection; } void TreeOperations::setConnection(QSharedPointer c) { m_connection = c; m_events->registerLoggerForConnection(*c); } ServerConfig TreeOperations::config() { m_config.setOwner(sharedFromThis().toWeakRef()); return m_config; } void TreeOperations::setConfig(const ServerConfig &c) { m_config = c; m_config.setOwner(sharedFromThis().toWeakRef()); m_connection->setConnectionConfig(m_config); emit configUpdated(); } void TreeOperations::proceedWithSecret(const ServerConfig &c) { m_config = c; m_config.setOwner(sharedFromThis().toWeakRef()); m_connection->setConnectionConfig(m_config); if (m_pendingOperation) { m_pendingOperation(m_connection); } else { qWarning() << "Unknown proceedWithSecret request"; return; } } QString TreeOperations::iconColor() { return m_config.iconColor(); } ================================================ FILE: src/app/models/treeoperations.h ================================================ #pragma once #include #include #include #include #include "app/models/connectionconf.h" #include "connections-tree/items/keyitem.h" #include "modules/bulk-operations/bulkoperationsmanager.h" #include "modules/connections-tree/operations.h" class Events; namespace ConnectionsTree { class ServerItem; class TreeItem; } // namespace ConnectionsTree class TreeOperations : public QObject, public ConnectionsTree::Operations, public QEnableSharedFromThis { Q_OBJECT public: TreeOperations(const ServerConfig& config, QSharedPointer events); QFuture getDatabases( QSharedPointer callback) override; void loadNamespaceItems( uint dbIndex, const QString& filter, QSharedPointer callback) override; void disconnect() override; void resetConnection() override; QString getNamespaceSeparator() override; QString defaultFilter() override; QVariantMap getFilterHistory() override; QString connectionName() const override; void openKeyTab(QSharedPointer key, bool openInNewTab = false) override; void openConsoleTab(int dbIndex = 0) override; void openNewKeyDialog(int dbIndex, QSharedPointer callback, QString keyPrefix = QString()) override; void openServerStats() override; void duplicateConnection() override; void notifyDbWasUnloaded(int dbIndex) override; void deleteDbKey(ConnectionsTree::KeyItem& key, QSharedPointer callback) override; virtual void deleteDbKeys(ConnectionsTree::DatabaseItem& db) override; void deleteDbNamespace(ConnectionsTree::NamespaceItem& ns) override; virtual void setTTL(ConnectionsTree::AbstractNamespaceItem& ns) override; virtual void copyKeys(ConnectionsTree::AbstractNamespaceItem& ns) override; virtual void importKeysFromRdb(ConnectionsTree::DatabaseItem& ns) override; virtual void flushDb(int dbIndex, QSharedPointer callback) override; virtual QFuture connectionSupportsMemoryOperations() override; virtual void openKeyIfExists( const QByteArray& key, QSharedPointer parent, QSharedPointer callback) override; virtual void getUsedMemory(const QList& keys, int dbIndex, QSharedPointer result, QSharedPointer progress) override; virtual QString mode() override; virtual bool isConnected() const override; QSharedPointer connection(); void setConnection(QSharedPointer c); ServerConfig config(); void setConfig(const ServerConfig& c); void proceedWithSecret(const ServerConfig& c); QString iconColor() override; signals: void createNewConnection(const ServerConfig& config); void configUpdated(); void filterHistoryUpdated(); void secretRequired(const ServerConfig& config, const QString& id); protected: void loadDatabases( QSharedPointer c, QSharedPointer> d, std::function callback); void recursiveSelectScan( QSharedPointer> d, QSharedPointer c, QSharedPointer dbList, std::function callback); bool connect(QSharedPointer c); void requestBulkOperation( ConnectionsTree::AbstractNamespaceItem& ns, BulkOperations::Manager::Operation op, BulkOperations::AbstractOperation::OperationCallback callback); private: typedef std::function)> PendingOperation; void getReadyConnection(PendingOperation callback); private: QSharedPointer m_connection; QSharedPointer m_events; uint m_dbCount; RedisClient::Connection::Mode m_connectionMode; ServerConfig m_config; QVariantMap m_filterHistory; QWeakPointer m_serverItem; QSharedPointer> m_dbScanOp; PendingOperation m_pendingOperation; }; ================================================ FILE: src/app/qcompress.cpp ================================================ #include "qcompress.h" #include #include #include #include #include #include #include #include #define ZLIB_WINDOW_BIT 15 + 16 #define ZLIB_PHP_WINDOW_BIT 15 #define ZLIB_CHUNK_SIZE 32 * 1024 #define ZLIB_LEVEL 6 #define ZSTD_LEVEL 1 #define BROTLI_BUFFER_SIZE 32 * 1024 struct LZ4FCleanUp { static inline void cleanup(LZ4F_dctx *p) { LZ4F_freeDecompressionContext(p); } }; struct ZSTDCleanUp { static inline void cleanup(ZSTD_DCtx *p) { ZSTD_freeDCtx(p); } }; QByteArray gzipDecode(const QByteArray &val, int windowBits) { z_stream strm; strm.zalloc = Z_NULL; strm.zfree = Z_NULL; strm.opaque = Z_NULL; strm.avail_in = 0; strm.next_in = Z_NULL; QByteArray output; int ret = inflateInit2(&strm, windowBits); if (ret != Z_OK) return QByteArray(); const char *input_data = val.data(); int input_data_left = val.length(); do { int chunk_size = qMin(ZLIB_CHUNK_SIZE, input_data_left); if (chunk_size <= 0) break; strm.next_in = (unsigned char *)input_data; strm.avail_in = chunk_size; input_data += chunk_size; input_data_left -= chunk_size; do { char out[ZLIB_CHUNK_SIZE]; strm.next_out = (unsigned char *)out; strm.avail_out = ZLIB_CHUNK_SIZE; ret = inflate(&strm, Z_NO_FLUSH); switch (ret) { case Z_NEED_DICT: ret = Z_DATA_ERROR; case Z_DATA_ERROR: case Z_MEM_ERROR: case Z_STREAM_ERROR: inflateEnd(&strm); return QByteArray(); } int have = (ZLIB_CHUNK_SIZE - strm.avail_out); if (have > 0) output.append((char *)out, have); } while (strm.avail_out == 0); } while (ret != Z_STREAM_END); inflateEnd(&strm); if (ret == Z_STREAM_END) { return output; } else { return QByteArray(); } } QByteArray lz4RawDecode(const QByteArray &val) { int offset = sizeof(int); if (val.size() < offset) { return QByteArray(); } int dataSize; memcpy(&dataSize, val.data(), offset); QByteArray dst(dataSize, '\x00'); auto res = LZ4_decompress_safe(val.constData() + offset, dst.data(), val.size() - offset, dst.capacity()); if (res < 0) { qWarning() << "LZ4 raw decoding error"; return QByteArray(); } return dst; } QByteArray lz4RawEncode(const QByteArray &val) { int maxSize = LZ4_compressBound(val.size()); QByteArray dst(maxSize, '\x00'); int res = LZ4_compress_default(val.constData(), dst.data(), val.size(), dst.capacity()); if (res == 0) { qWarning() << "LZ4 raw decoding error"; return QByteArray(); } return dst; } QByteArray lz4FrameDecode(const QByteArray &val) { LZ4F_dctx *lz4_dctx = nullptr; LZ4F_createDecompressionContext(&lz4_dctx, LZ4F_VERSION); if (!lz4_dctx) { qWarning() << "LZ4 error. Cannot initialize context"; return QByteArray(); } QScopedPointer dctx(lz4_dctx); LZ4F_frameInfo_t lz4_frameinfo; size_t buffSize = val.size(); size_t res = LZ4F_getFrameInfo(dctx.data(), &lz4_frameinfo, static_cast(val.constData()), static_cast(&buffSize)); if (LZ4F_isError(res)) { qWarning() << "LZ4 error. Cannot retrive frame info"; return QByteArray(); } size_t contentSize = lz4_frameinfo.contentSize; size_t srcSize = val.size(); if (!(0 < contentSize && contentSize <= 255 * srcSize)) { return QByteArray(); } QByteArray dst(contentSize, '\x00'); size_t dstSize = dst.size(); static constexpr const LZ4F_decompressOptions_t opt{}; res = LZ4F_decompress(dctx.data(), dst.data(), &dstSize, val.data() + buffSize, &srcSize, &opt); if (LZ4F_isError(res)) { qWarning() << "LZ4 error. Cannot decode frame" << LZ4F_getErrorName(res); return QByteArray(); } return dst; } QByteArray gzipEncode(const QByteArray &val, int windowBits) { int flush = 0; z_stream strm; strm.zalloc = Z_NULL; strm.zfree = Z_NULL; strm.opaque = Z_NULL; strm.avail_in = 0; strm.next_in = Z_NULL; QByteArray output; int ret = deflateInit2(&strm, qMax(-1, qMin(9, ZLIB_LEVEL)), Z_DEFLATED, windowBits, 8, Z_DEFAULT_STRATEGY); if (ret != Z_OK) return output; const char *input_data = val.data(); int input_data_left = val.length(); do { int chunk_size = qMin(ZLIB_CHUNK_SIZE, input_data_left); strm.next_in = (unsigned char *)input_data; strm.avail_in = chunk_size; input_data += chunk_size; input_data_left -= chunk_size; flush = (input_data_left <= 0 ? Z_FINISH : Z_NO_FLUSH); do { char out[ZLIB_CHUNK_SIZE]; strm.next_out = (unsigned char *)out; strm.avail_out = ZLIB_CHUNK_SIZE; ret = deflate(&strm, flush); if (ret == Z_STREAM_ERROR) { deflateEnd(&strm); return QByteArray(); } int have = (ZLIB_CHUNK_SIZE - strm.avail_out); if (have > 0) output.append((char *)out, have); } while (strm.avail_out == 0); } while (flush != Z_FINISH); (void)deflateEnd(&strm); if (ret == Z_STREAM_END) { return output; } else { return QByteArray(); } } QByteArray lz4FrameEncode(const QByteArray &val) { QByteArray dst; LZ4F_preferences_t opt{}; opt.frameInfo.contentSize = val.size(); size_t expectedSize = LZ4F_compressFrameBound(val.size(), &opt); dst.resize(expectedSize); size_t res = LZ4F_compressFrame(dst.data(), dst.size(), val.data(), val.size(), &opt); if (LZ4F_isError(res)) { qWarning() << "LZ4 error. Cannot compress frame" << LZ4F_getErrorName(res); return QByteArray(); } if (expectedSize > res) { dst.resize(res); } return dst; } QByteArray zstdDecode(const QByteArray &val) { size_t buffSize = val.size(); auto decompressedSize = ZSTD_getFrameContentSize( static_cast(val.constData()), buffSize); if (decompressedSize == 0UL || decompressedSize == ZSTD_CONTENTSIZE_ERROR) { return QByteArray(); } size_t srcSize = val.size(); ZSTD_DCtx *const zstd_dctx = ZSTD_createDCtx(); if (!zstd_dctx) { qWarning() << "ZSTD error. Cannot initialize context"; return QByteArray(); } QScopedPointer dctx(zstd_dctx); QByteArray dst(decompressedSize, '\x00'); size_t dstSize = dst.size(); size_t const res = ZSTD_decompress(static_cast(dst.data()), dstSize, static_cast(val.data()), srcSize); if (ZSTD_isError(res)) { qWarning() << "ZSTD error. Cannot decode frame" << ZSTD_getErrorName(res); return QByteArray(); } dst.resize(res); return dst; } QByteArray zstdEncode(const QByteArray &val) { QByteArray dst; dst.resize(ZSTD_compressBound(val.size())); size_t res = ZSTD_compress(dst.data(), dst.size(), val.data(), val.size(), ZSTD_LEVEL); if (ZSTD_isError(res)) { qWarning() << "ZSTD error. Cannot compress frame" << ZSTD_getErrorName(res); return QByteArray(); } dst.resize(res); return dst; } QByteArray snappyDecode(const QByteArray &val) { size_t size = 0; bool res = snappy::GetUncompressedLength(val.constData(), val.size(), &size); if (!res) { qWarning() << "Snappy error: Cannot get uncompressed size"; QByteArray(); } std::string output; res = snappy::Uncompress(val.constData(), val.size(), &output); if (!res) { qWarning() << "Snappy error: Cannot uncompress buffer"; QByteArray(); } return QByteArray::fromStdString(output); } QByteArray snappyEncode(const QByteArray &val) { std::string output; bool res = snappy::Compress(val.constData(), val.size(), &output); if (!res) { qWarning() << "Snappy error: Cannot compress buffer"; QByteArray(); } return QByteArray::fromStdString(output); } QByteArray brotliDecode(const QByteArray &val) { auto decoder = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr); if (!decoder) { qWarning() << "BROTLI: Cannot create decoder"; return QByteArray(); } QByteArray dst; dst.resize(BROTLI_BUFFER_SIZE); size_t availableIn = val.size(), availableOut = dst.size(); const uint8_t* nextIn = reinterpret_cast(val.constData()); uint8_t* nextOut = reinterpret_cast(dst.data()); BrotliDecoderResult itResult; do { itResult = BrotliDecoderDecompressStream( decoder, &availableIn, &nextIn, &availableOut, &nextOut, nullptr); if (itResult == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { size_t offset = dst.size() - availableOut; availableOut += dst.size(); dst.resize(dst.size() * 2); nextOut = reinterpret_cast(dst.data()) + offset; itResult = BROTLI_DECODER_RESULT_SUCCESS; } if (itResult != BROTLI_DECODER_RESULT_SUCCESS) { qWarning() << "Brotli: Invalid input"; return QByteArray(); } } while (!(availableIn == 0 && itResult == BROTLI_DECODER_RESULT_SUCCESS)); if (itResult == BROTLI_DECODER_RESULT_SUCCESS) dst.resize(dst.size() - availableOut); BrotliDecoderDestroyInstance(decoder); return dst; } QByteArray brotliEncode(const QByteArray &val) { auto encoder = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); if (!encoder) { qWarning() << "BROTLI: Cannot create encoder"; return QByteArray(); } QByteArray dst; dst.resize(BROTLI_BUFFER_SIZE); size_t availableIn = val.size(), available_out = dst.size(); const uint8_t* nextIn = reinterpret_cast(val.constData()); uint8_t* nextOut = reinterpret_cast(dst.data()); size_t totalOut = 0; int itResult; do { itResult = BrotliEncoderCompressStream ( encoder, BROTLI_OPERATION_FINISH, &availableIn, &nextIn, &available_out, &nextOut, &totalOut ); } while (!(availableIn == 0 && BrotliEncoderIsFinished(encoder))); if (itResult == BROTLI_TRUE) { dst.resize(totalOut); } BrotliEncoderDestroyInstance(encoder); return dst; } bool validateLZ4Frame(const QByteArray &val) { const auto magicHeader = QByteArray::fromHex("x04x22x4dx18"); if (!val.startsWith(magicHeader)) return false; LZ4F_dctx *lz4_dctx = nullptr; LZ4F_createDecompressionContext(&lz4_dctx, LZ4F_VERSION); if (!lz4_dctx) { qWarning() << "LZ4 error. Cannot initialize context"; return false; } QScopedPointer dctx(lz4_dctx); LZ4F_frameInfo_t lz4_frameinfo; size_t buffSize = val.size(); size_t res = LZ4F_getFrameInfo(dctx.data(), &lz4_frameinfo, static_cast(val.constData()), static_cast(&buffSize)); if (LZ4F_isError(res)) { qWarning() << "LZ4 error. Cannot retrive frame info"; return false; } return lz4_frameinfo.contentSize > 0; } bool validateZSTDFrame(const QByteArray &val) { const auto magicHeader = QByteArray::fromHex("x28xB5x2FxFD"); if (!val.startsWith(magicHeader)) { return false; } size_t buffSize = val.size(); unsigned long long decompressedSize = ZSTD_getFrameContentSize( static_cast(val.constData()), buffSize); return !(decompressedSize == ZSTD_CONTENTSIZE_ERROR); } bool validateGZip(const QByteArray &val, int from = 0) { return val.indexOf(QByteArray::fromHex("x1fx8b"), from) == 0; } bool validateSnappyFrame(const QByteArray &val) { return snappy::IsValidCompressedBuffer(val.constData(), val.size()); } bool isMagentoCacheFormat(unsigned f) { return qcompress::MAGENTO_CACHE_GZIP <= f && f <= qcompress::MAGENTO_CACHE_SNAPPY; } QHash knownMagentoFormats() { return { {qcompress::MAGENTO_CACHE_GZIP, "gz"}, {qcompress::MAGENTO_SESSION_GZIP, "gz"}, {qcompress::MAGENTO_CACHE_LZ4, "l4"}, {qcompress::MAGENTO_SESSION_LZ4, "l4"}, {qcompress::MAGENTO_CACHE_ZSTD, "zs"}, {qcompress::MAGENTO_SESSION_SNAPPY, "sn"}, {qcompress::MAGENTO_CACHE_SNAPPY, "sn"}, }; } static const QHash magentoFormats = knownMagentoFormats(); QByteArray magentoPrefix(unsigned f) { if (!magentoFormats.contains(f)) { return QByteArray(); } QByteArray id = magentoFormats[f]; if (isMagentoCacheFormat(f)) { id += QByteArray(":") + QByteArray::fromHex("x1fx8b"); } else { id = QByteArray(":") + id + QByteArray(":"); } return id; } unsigned qcompress::guessFormat(const QByteArray &val) { if (val.size() > 4) { auto mFormats = magentoFormats.keys(); QByteArray prefix; for (auto f : qAsConst(mFormats)) { prefix = magentoPrefix(f); if (val.startsWith(prefix)) { return f; } } } if (val.size() > 2 && validateGZip(val)) { return qcompress::GZIP; } else if (val.size() > 4 && validateLZ4Frame(val)) { return qcompress::LZ4; } else if (val.size() > 4 && validateZSTDFrame(val)) { return qcompress::ZSTD; } else if (val.size() > 10 && validateSnappyFrame(val)) { return qcompress::SNAPPY; } return qcompress::UNKNOWN; } QByteArray qcompress::compress(const QByteArray &val, unsigned algo) { switch (algo) { case qcompress::GZIP: return gzipEncode(val, ZLIB_WINDOW_BIT); case qcompress::MAGENTO_SESSION_GZIP: case qcompress::MAGENTO_CACHE_GZIP: return magentoPrefix(algo) + gzipEncode(val, ZLIB_PHP_WINDOW_BIT); case qcompress::LZ4: return lz4FrameEncode(val); case qcompress::MAGENTO_SESSION_LZ4: case qcompress::MAGENTO_CACHE_LZ4: return magentoPrefix(algo) + lz4RawEncode(val); case qcompress::ZSTD: return zstdEncode(val); case qcompress::MAGENTO_CACHE_ZSTD: return magentoPrefix(algo) + zstdEncode(val); case qcompress::SNAPPY: return snappyEncode(val); case qcompress::MAGENTO_CACHE_SNAPPY: case qcompress::MAGENTO_SESSION_SNAPPY: return magentoPrefix(algo) + snappyEncode(val); case qcompress::BROTLI: return brotliEncode(val); default: return QByteArray(); } } QByteArray qcompress::decompress(const QByteArray &val, unsigned format) { int offset = 0; if (magentoFormats.contains(format)) { offset = magentoPrefix(format).size(); } switch (format) { case qcompress::GZIP: return gzipDecode(val, ZLIB_WINDOW_BIT); case qcompress::MAGENTO_SESSION_GZIP: case qcompress::MAGENTO_CACHE_GZIP: case qcompress::GZIP_PHP: return gzipDecode(val.mid(offset), ZLIB_PHP_WINDOW_BIT); case qcompress::LZ4: return lz4FrameDecode(val); case qcompress::MAGENTO_SESSION_LZ4: case qcompress::MAGENTO_CACHE_LZ4: case qcompress::LZ4_RAW: return lz4RawDecode(val.mid(offset)); case qcompress::ZSTD: return zstdDecode(val); case qcompress::MAGENTO_CACHE_ZSTD: return zstdDecode(val.mid(offset)); case qcompress::SNAPPY: return snappyDecode(val); case qcompress::MAGENTO_CACHE_SNAPPY: case qcompress::MAGENTO_SESSION_SNAPPY: return snappyDecode(val.mid(offset)); case qcompress::BROTLI: return brotliDecode(val); default: return QByteArray(); } } QString qcompress::nameOf(unsigned alg) { switch (alg) { case qcompress::GZIP: return "gzip"; case qcompress::LZ4: return "lz4"; case qcompress::MAGENTO_SESSION_GZIP: return "magento-session-gzip"; case qcompress::MAGENTO_SESSION_LZ4: return "magento-session-lz4"; case qcompress::MAGENTO_CACHE_GZIP: return "magento-cache-gzip"; case qcompress::MAGENTO_CACHE_LZ4: return "magento-cache-lz4"; case qcompress::MAGENTO_CACHE_ZSTD: return "magento-cache-zstd"; case qcompress::MAGENTO_CACHE_SNAPPY: return "magento-cache-snappy"; case qcompress::MAGENTO_SESSION_SNAPPY: return "magento-session-snappy"; case qcompress::ZSTD: return "ZSTD"; case qcompress::SNAPPY: return "Snappy"; case qcompress::GZIP_PHP: return "PHP gzcompress"; case qcompress::LZ4_RAW: return "LZ4 Raw"; case qcompress::BROTLI: return "Brotli"; case qcompress::UNKNOWN: default: return "unknown"; } } ================================================ FILE: src/app/qcompress.h ================================================ #pragma once #include #include namespace qcompress { enum { UNKNOWN, GZIP, LZ4, ZSTD, /* * MAGENTO session is build on top of the following php lib to store *compressed sessions: * https://github.com/colinmollenhour/php-redis-session-abstract/blob/5f399c53534cd1fe07460407e510590840b2c6d0/src/Cm/RedisSession/Handler.php#L822-L828 **/ MAGENTO_SESSION_GZIP, MAGENTO_SESSION_LZ4, MAGENTO_SESSION_SNAPPY, // MAGENTO_SESSION_ZSTD, // ZSTD is not yet supported - // https://github.com/colinmollenhour/php-redis-session-abstract/issues/42 /* * MAGENTO CACHE * https://github.com/colinmollenhour/Cm_Cache_Backend_Redis/blob/a9c4a5ae6001e04097aa7302abd89c9496c563e0/Cm/Cache/Backend/Redis.php#L1202-L1207 */ MAGENTO_CACHE_GZIP, MAGENTO_CACHE_LZ4, MAGENTO_CACHE_ZSTD, MAGENTO_CACHE_SNAPPY, SNAPPY, GZIP_PHP, LZ4_RAW, BROTLI }; unsigned guessFormat(const QByteArray& val); QString nameOf(unsigned alg); QByteArray decompress(const QByteArray& val, unsigned algo); QByteArray compress(const QByteArray& val, unsigned algo); } // namespace qcompress ================================================ FILE: src/app/qmlutils.cpp ================================================ #include "qmlutils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "apputils.h" #include "jsonutils.h" #include "qcompress.h" #include "value-editor/largetextmodel.h" #define MAX_CHART_DATA_POINTS 1000 bool QmlUtils::isBinaryString(const QVariant &value) { if (!value.canConvert(QVariant::ByteArray)) { return false; } QByteArray val = value.toByteArray(); return isBinary(val); } long QmlUtils::binaryStringLength(const QVariant &value) { if (!value.canConvert(QVariant::ByteArray)) { return -1; } QByteArray val = value.toByteArray(); return val.size(); } QVariant QmlUtils::b64toByteArray(const QVariant &value) { if (!value.canConvert(QVariant::String)) { return -1; } return QVariant(QByteArray::fromBase64(value.toString().toUtf8())); } QByteArray QmlUtils::minifyJSON(const QVariant &value) { if (!value.canConvert(QVariant::ByteArray)) { return QByteArray(); } QByteArray val = value.toByteArray(); return JSONUtils::minifyJSON(val); } QByteArray QmlUtils::prettyPrintJSON(const QVariant &value) { if (!value.canConvert(QVariant::ByteArray)) { return QByteArray(); } QByteArray val = value.toByteArray(); return JSONUtils::prettyPrintJSON(val); } bool QmlUtils::isJSON(const QVariant &value) { if (!value.canConvert(QVariant::ByteArray)) { return false; } QByteArray val = value.toByteArray(); return JSONUtils::isJSON(val); } QVariant QmlUtils::decompress(const QVariant &value, unsigned alg) { if (!value.canConvert(QVariant::ByteArray)) { return 0; } return qcompress::decompress(value.toByteArray(), alg); } QVariant QmlUtils::compress(const QVariant &value, unsigned alg) { return qcompress::compress(value.toByteArray(), alg); } unsigned QmlUtils::isCompressed(const QVariant &value) { if (!value.canConvert(QVariant::ByteArray)) { return 0; } return qcompress::guessFormat(value.toByteArray()); } QString QmlUtils::compressionAlgName(unsigned alg) { return qcompress::nameOf(alg); } QVariant QmlUtils::compressionMethodsNoMagic() { QVariantList methodsWithoutMagicHeaders = { qcompress::UNKNOWN, qcompress::BROTLI, qcompress::LZ4_RAW, //NOTE(u_glide): Should be always last in this list // QML side use it for conditions qcompress::GZIP_PHP, }; return methodsWithoutMagicHeaders; } QString QmlUtils::humanSize(long size) { return humanReadableSize(size); } QVariant QmlUtils::valueToBinary(const QVariant &value) { if (!value.canConvert(QVariant::ByteArray)) { return QVariant(); } QByteArray val = value.toByteArray(); QVariantList list; for (int index = 0; index < val.length(); ++index) { list.append(QVariant((unsigned char)val.at(index))); } return QVariant(list); } QVariant QmlUtils::binaryListToValue(const QVariantList &binaryList) { QByteArray value; foreach (QVariant v, binaryList) { value.append((unsigned char)v.toInt()); } return value; } QVariant QmlUtils::printable(const QVariant &value, bool htmlEscaped, int maxLength) { if (!value.canConvert(QVariant::ByteArray)) { return QVariant(); } QByteArray val = value.toByteArray(); if (maxLength > 0 && val.size() > maxLength) { val.truncate(maxLength); } if (htmlEscaped) { return printableString(val).toHtmlEscaped(); } else { return printableString(val); } } QVariant QmlUtils::printableToValue(const QVariant &printable) { if (!printable.canConvert(QVariant::String)) { return QVariant(); } QString val = printable.toString(); return printableStringToBinary(val); } QVariant QmlUtils::toUtf(const QVariant &value) { if (!value.canConvert(QVariant::ByteArray)) { return QVariant(); } QByteArray val = value.toByteArray(); QString result = QString::fromUtf8(val.constData(), val.size()); return QVariant(result); } QString QmlUtils::getNativePath(const QString &path) { return QDir::toNativeSeparators(path); } QString QmlUtils::getPathFromUrl(const QUrl &url) { return url.isLocalFile() ? url.toLocalFile() : url.path(); } QString QmlUtils::getUrlFromPath(const QString &path) { return QUrl::fromLocalFile(path).toString(); } QString QmlUtils::getDir(const QString &path) { return QFileInfo(path).absoluteDir().absolutePath(); } bool QmlUtils::fileExists(const QString &path) { return QFileInfo::exists(path); } QString QmlUtils::replaceColorsInSvg(const QString &path, QVariant mapping) { QFile svgFile(path.mid(3)); if (!svgFile.open(QIODevice::ReadOnly)) { qWarning() << "Cannot open svg:" << path.mid(3); return QString(); } if (!mapping.canConvert()) { qWarning() << "Invalid colors mapping:" << mapping; return QString(); } QVariantMap colors = mapping.toMap(); QString svgData = QString::fromUtf8(svgFile.readAll()); auto it = colors.constBegin(); while (it != colors.constEnd()) { auto originalColor = it.key(); auto newColor = QColor(it.value().toString()); if (newColor.alphaF() < 1) { svgData.replace(originalColor, QString("%1\" fill-opacity=\"%2").arg(newColor.name()).arg(newColor.alphaF())); } else { svgData = svgData.replace(originalColor, newColor.name()); } ++it; } return QString("data:image/svg+xml;utf8,%1").arg(svgData); } QString QmlUtils::changeColorAlpha(QColor c, int a) { c.setAlpha(a); return c.name(QColor::HexArgb); } void QmlUtils::copyToClipboard(const QString &text) { QClipboard *cb = QApplication::clipboard(); if (!cb) return; cb->clear(); cb->setText(text); } bool QmlUtils::saveToFile(const QVariant &value, const QString &path) { if (!value.canConvert(QVariant::ByteArray)) { return false; } QtConcurrent::run([value, path]() { QByteArray val = value.toByteArray(); QFile outputFile(path); if (outputFile.open(QIODevice::WriteOnly)) { QDataStream outStream(&outputFile); outStream.writeRawData(val, val.size()); outputFile.close(); return true; } return false; }); return true; } QtCharts::QDateTimeAxis *findDateTimeAxis(QtCharts::QXYSeries *series) { using namespace QtCharts; QList axes = series->attachedAxes(); QDateTimeAxis *ax = nullptr; for (QAbstractAxis *axis : axes) { if (axis->type() == QAbstractAxis::AxisTypeDateTime) { ax = qobject_cast(axis); return ax; } } return ax; } void QmlUtils::addNewValueToDynamicChart(QtCharts::QXYSeries *series, qreal value) { using namespace QtCharts; QDateTimeAxis *ax = findDateTimeAxis(series); if (!(ax && series)) { qWarning() << "Cannot add value to dynamic chart. Invalid pointers."; return; } int totalPoints = series->count(); if (totalPoints == 0) { ax->setMin(QDateTime::currentDateTime()); } bool dataNotChangedLastFivePoints = totalPoints > 10; for (int i = 1; dataNotChangedLastFivePoints && i < 6; i++) { if (value != series->at(totalPoints - i).y()) { dataNotChangedLastFivePoints = false; break; } } if (dataNotChangedLastFivePoints) { series->replace(totalPoints - 1, QDateTime::currentDateTime().toMSecsSinceEpoch(), value); } else { series->append(QDateTime::currentDateTime().toMSecsSinceEpoch(), value); } if (totalPoints > MAX_CHART_DATA_POINTS) { series->removePoints(0, totalPoints - MAX_CHART_DATA_POINTS); ax->setMin(QDateTime::fromMSecsSinceEpoch(series->at(0).x())); } if (series->attachedAxes().size() > 0) { ax->setMax(QDateTime::currentDateTime()); } } QObject *QmlUtils::wrapLargeText(const QByteArray &text) { // NOTE(u_glide): Use 50Kb chunks by default int chunkSize = 50000; auto w = new ValueEditor::LargeTextWrappingModel(QString::fromUtf8(text), chunkSize); w->setParent(this); return w; } void QmlUtils::deleteTextWrapper(QObject *w) { if (w && w->parent() == this) { w->deleteLater(); } } QString QmlUtils::escapeHtmlEntities(const QString &t) { return t.toHtmlEscaped(); } QString QmlUtils::standardKeyToString(QKeySequence::StandardKey key) { return QKeySequence(key).toString(QKeySequence::NativeText); } double QmlUtils::getScreenScaleFactor() { return QApplication::primaryScreen()->logicalDotsPerInch() / 96; } bool QmlUtils::isAppStoreBuild() { #ifdef RDM_APPSTORE return true; #else return false; #endif } ================================================ FILE: src/app/qmlutils.h ================================================ #pragma once #include #include #include #include #include #include class QmlUtils : public QObject { Q_OBJECT public: Q_INVOKABLE bool isBinaryString(const QVariant &value); Q_INVOKABLE long binaryStringLength(const QVariant &value); Q_INVOKABLE QVariant b64toByteArray(const QVariant &value); Q_INVOKABLE QByteArray minifyJSON(const QVariant &value); Q_INVOKABLE QByteArray prettyPrintJSON(const QVariant &value); Q_INVOKABLE bool isJSON(const QVariant &value); Q_INVOKABLE unsigned isCompressed(const QVariant &value); Q_INVOKABLE QVariant decompress(const QVariant &value, unsigned alg); Q_INVOKABLE QVariant compress(const QVariant &value, unsigned alg); Q_INVOKABLE QString compressionAlgName(unsigned alg); Q_INVOKABLE QVariant compressionMethodsNoMagic(); Q_INVOKABLE QString humanSize(long size); Q_INVOKABLE QVariant valueToBinary(const QVariant &value); Q_INVOKABLE QVariant binaryListToValue(const QVariantList& binaryList); Q_INVOKABLE QVariant printable(const QVariant &value, bool htmlEscaped=false, int maxLength=-1); Q_INVOKABLE QVariant printableToValue(const QVariant &printable); Q_INVOKABLE QVariant toUtf(const QVariant &value); Q_INVOKABLE QString getNativePath(const QString &path); Q_INVOKABLE QString getPathFromUrl(const QUrl &url); Q_INVOKABLE QString getUrlFromPath(const QString &path); Q_INVOKABLE QString getDir(const QString &path); Q_INVOKABLE bool fileExists(const QString& path); Q_INVOKABLE QString replaceColorsInSvg(const QString& path, QVariant mapping); Q_INVOKABLE QString changeColorAlpha(QColor c, int a); Q_INVOKABLE void copyToClipboard(const QString &text); Q_INVOKABLE bool saveToFile(const QVariant &value, const QString &path); Q_INVOKABLE void addNewValueToDynamicChart(QtCharts::QXYSeries* series, qreal value); Q_INVOKABLE QObject* wrapLargeText(const QByteArray &text); Q_INVOKABLE void deleteTextWrapper(QObject* w); Q_INVOKABLE QString escapeHtmlEntities(const QString& t); Q_INVOKABLE QString standardKeyToString(QKeySequence::StandardKey key); Q_INVOKABLE double getScreenScaleFactor(); Q_INVOKABLE bool isAppStoreBuild(); }; ================================================ FILE: src/main.cpp ================================================ #include #include #include #include #include #if defined(Q_OS_WIN) | defined(Q_OS_LINUX) #include #define RELAUNCH_CODE 1001 #endif #ifdef CRASHPAD_INTEGRATION #include "crashpad/handler.h" #endif #ifdef LINUX_SIGNALS #include #endif #ifdef IGNORE_QML_WARNINGS static const QtMessageHandler QT_DEFAULT_MESSAGE_HANDLER = qInstallMessageHandler(0); void customMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { switch (type) { case QtWarningMsg: { if (!msg.contains("QML IconLabel")) { (*QT_DEFAULT_MESSAGE_HANDLER)(type, context, msg); } } break; default: // Call the default handler. (*QT_DEFAULT_MESSAGE_HANDLER)(type, context, msg); break; } } #endif #include "app/app.h" int main(int argc, char *argv[]) { int returnCode = 0; #ifdef CRASHPAD_INTEGRATION QFileInfo appPath(QString::fromLocal8Bit(argv[0])); QString appDir(appPath.absoluteDir().path()); startCrashpad(appDir); #endif #if defined(Q_OS_WIN) || defined(Q_OS_LINUX) bool disableAutoScaling = false; #ifndef DISABLE_SCALING_TEST { QGuiApplication tmp(argc, argv); disableAutoScaling = QGuiApplication::primaryScreen() && QGuiApplication::primaryScreen()->availableSize().width() <= 1920 && QGuiApplication::primaryScreen()->devicePixelRatio() == 1; } #endif if (disableAutoScaling) { qDebug() << "Disable auto-scaling"; QGuiApplication::setAttribute(Qt::AA_DisableHighDpiScaling); } else { qDebug() << "Enable auto-scaling"; QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); } #endif Application a(argc, argv); #ifdef IGNORE_QML_WARNINGS qInstallMessageHandler(customMessageHandler); #endif #ifdef LINUX_SIGNALS UnixSignalWatcher sigwatch; sigwatch.watchForSignal(SIGINT); sigwatch.watchForSignal(SIGTERM); QObject::connect(&sigwatch, SIGNAL(unixSignal(int)), &a, SLOT(quit())); #endif a.initModels(); a.initQml(); returnCode = a.exec(); #if defined(Q_OS_WIN) | defined(Q_OS_LINUX) if (returnCode == RELAUNCH_CODE) { QProcess::startDetached(a.arguments()[0], a.arguments()); returnCode = 0; } #endif return returnCode; } ================================================ FILE: src/modules/bulk-operations/bulkoperationsmanager.cpp ================================================ #include "bulkoperationsmanager.h" #include #include #include #include "operations/copyoperation.h" #include "operations/deleteoperation.h" #include "operations/rdbimport.h" #include "operations/ttloperation.h" BulkOperations::Manager::Manager(QSharedPointer model) : QObject(nullptr), m_model(model), m_python(nullptr) { Q_ASSERT(m_model); } void BulkOperations::Manager::setPython(QSharedPointer p) { if (!p) { qWarning() << "Invalid python instance passed to BulkOperations"; return; } m_python = p; } bool BulkOperations::Manager::hasOperation() const { return !m_operation.isNull(); } bool BulkOperations::Manager::multiConnectionOperation() const { return m_operation && m_operation->multiConnectionOperation(); } bool BulkOperations::Manager::clearOperation() { if (!hasOperation()) return true; if (m_operation->isRunning()) { return false; } m_operation.clear(); return true; } void BulkOperations::Manager::runOperation(int connectionIndex, int dbIndex) { if (!hasOperation()) return; if (m_operation->multiConnectionOperation()) { if (!(connectionIndex >= 0 && dbIndex >= 0 && m_model->getByIndex(connectionIndex))) { qWarning() << "invalid target connection"; return; } m_operation->run(m_model->getByIndex(connectionIndex), dbIndex); } else { m_operation->run(); } } void BulkOperations::Manager::getAffectedKeys() { if (!hasOperation()) return; m_operation->getAffectedKeys([this](QVariant r, QString e) { if (!e.isEmpty()) { emit error(e, ""); return; } emit affectedKeys(r); }); } QVariant BulkOperations::Manager::getTargetConnections() { return QVariant(m_model->getConnections()); } void BulkOperations::Manager::setOperationMetadata(const QVariantMap& meta) { if (hasOperation()) m_operation->setMetadata(meta); } QString BulkOperations::Manager::operationName() const { if (!hasOperation()) return QString(); return m_operation->getTypeName(); } QString BulkOperations::Manager::connectionName() const { if (!hasOperation()) return QString(); return m_operation->getConnection()->getConfig().name(); } int BulkOperations::Manager::dbIndex() const { if (!hasOperation()) return -1; return m_operation->getDbIndex(); } QString BulkOperations::Manager::keyPattern() const { if (!hasOperation()) return QString(); return m_operation->getKeyPattern().pattern(); } void BulkOperations::Manager::setKeyPattern(const QString& p) { if (!hasOperation()) return; m_operation->setKeyPattern(QRegExp(p, Qt::CaseSensitive, QRegExp::Wildcard)); } int BulkOperations::Manager::operationProgress() const { if (!hasOperation()) return -1; return m_operation->currentProgress(); } void BulkOperations::Manager::requestBulkOperation( QSharedPointer connection, int dbIndex, BulkOperations::Manager::Operation op, QRegExp keyPattern, AbstractOperation::OperationCallback callback) { if (hasOperation()) { qWarning() << "BulkOperationsManager already has bulk operation request"; return; } auto callbackWrapper = [this, callback](QRegExp filter, long processed, const QStringList& e) { if (e.size() > 0) { emit error(QCoreApplication::translate( "RESP", "Failed to perform actions on %1 keys. ") .arg(e.size()), e.join("\n")); } else { emit operationFinished(); } return callback(filter, processed, e); }; if (op == Operation::DELETE_KEYS) { m_operation = QSharedPointer( new BulkOperations::DeleteOperation(connection, dbIndex, callbackWrapper, keyPattern)); } else if (op == Operation::TTL) { m_operation = QSharedPointer( new BulkOperations::TtlOperation(connection, dbIndex, callbackWrapper, keyPattern)); } else if (op == Operation::COPY_KEYS) { m_operation = QSharedPointer( new BulkOperations::CopyOperation(connection, dbIndex, callbackWrapper, keyPattern)); } else if (op == Operation::IMPORT_RDB_KEYS) { if (!m_python) { qWarning() << "Python is not ready yet"; return; } m_operation = QSharedPointer( new BulkOperations::RDBImportOperation( connection, dbIndex, callbackWrapper, m_python, keyPattern)); } QObject::connect(m_operation.data(), &BulkOperations::AbstractOperation::progress, this, [this](int) { emit operationProgressChanged(); }); emit operationNameChanged(); emit connectionNameChanged(); emit dbIndexChanged(); emit keyPatternChanged(); emit operationProgressChanged(); emit openDialog(m_operation->getTypeName()); } ================================================ FILE: src/modules/bulk-operations/bulkoperationsmanager.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include "connections.h" #include "operations/abstractoperation.h" class QPython; namespace BulkOperations { class Manager : public QObject { Q_OBJECT Q_PROPERTY( QString operationName READ operationName NOTIFY operationNameChanged) Q_PROPERTY( QString connectionName READ connectionName NOTIFY connectionNameChanged) Q_PROPERTY(int dbIndex READ dbIndex NOTIFY dbIndexChanged) Q_PROPERTY(QString keyPattern READ keyPattern WRITE setKeyPattern NOTIFY keyPatternChanged) Q_PROPERTY(int operationProgress READ operationProgress NOTIFY operationProgressChanged) public: enum class Operation { DELETE_KEYS, COPY_KEYS, IMPORT_RDB_KEYS, TTL, }; public: Manager(QSharedPointer model); void setPython(QSharedPointer p); Q_INVOKABLE bool hasOperation() const; Q_INVOKABLE bool multiConnectionOperation() const; Q_INVOKABLE bool clearOperation(); Q_INVOKABLE void runOperation(int targetConnection = -1, int targetDb = -1); Q_INVOKABLE void getAffectedKeys(); Q_INVOKABLE QVariant getTargetConnections(); Q_INVOKABLE void setOperationMetadata(const QVariantMap& meta); // Property getters QString operationName() const; QString connectionName() const; int dbIndex() const; QString keyPattern() const; void setKeyPattern(const QString& p); int operationProgress() const; signals: void openDialog(const QString& operationName); void affectedKeys(QVariant r); void operationFinished(); void error(const QString& e, const QString& details); // Property notifiers void operationNameChanged(); void connectionNameChanged(); void dbIndexChanged(); void keyPatternChanged(); void operationProgressChanged(); public slots: void requestBulkOperation(QSharedPointer connection, int dbIndex, Operation op, QRegExp keyPattern, AbstractOperation::OperationCallback callback); private: QSharedPointer m_operation; QSharedPointer m_model; QSharedPointer m_python; }; } // namespace BulkOperations ================================================ FILE: src/modules/bulk-operations/connections.h ================================================ #pragma once #include #include namespace RedisClient { class Connection; } namespace BulkOperations { class ConnectionsModel { public: virtual QSharedPointer getByIndex(int index) = 0; virtual QStringList getConnections() = 0; }; } // namespace BulkOperations ================================================ FILE: src/modules/bulk-operations/operations/abstractoperation.cpp ================================================ #include "abstractoperation.h" #include BulkOperations::AbstractOperation::AbstractOperation( QSharedPointer connection, int dbIndex, OperationCallback callback, QRegExp keyPattern) : m_connection(connection), m_dbIndex(dbIndex), m_keyPattern(keyPattern), m_currentState(State::READY), m_progress(0), m_callback(callback), m_lastProgressNotification(0) {} void BulkOperations::AbstractOperation::getAffectedKeys( std::function callback) { auto processingCallback = [this, callback](const RedisClient::Connection::RawKeysList& keys, const QString& err) { if (!err.isEmpty()) { return callback(QVariant(), err); } m_affectedKeys.clear(); QStringList keyNames; for (const QByteArray &k : keys) { m_affectedKeys.append(k); keyNames.append(printableString(k, true)); } return callback(QVariant(keyNames), ""); }; try { if (!m_connection->connect(true)) { return callback(QVariant(), QCoreApplication::translate( "RESP", "Cannot connect to redis-server")); } if (m_connection->mode() == RedisClient::Connection::Mode::Cluster) { m_connection->getClusterKeys(processingCallback, m_keyPattern.pattern()); } else { m_connection->getDatabaseKeys(processingCallback, m_keyPattern.pattern(), m_dbIndex); } } catch (const RedisClient::Connection::Exception& e) { return callback(QVariant(), QString(e.what())); } } void BulkOperations::AbstractOperation::run( QSharedPointer targetConnection, int targetDbIndex) { if (!isMetadataValid()) { qWarning() << QString("Invalid metadata for %1").arg(getTypeName()); return; } if (m_affectedKeys.size() > 0) { performOperation(targetConnection, targetDbIndex); } else { getAffectedKeys([this, targetConnection, targetDbIndex](QVariant, QString) { performOperation(targetConnection, targetDbIndex); }); } } bool BulkOperations::AbstractOperation::isRunning() const { return m_currentState == State::RUNNING; } QSharedPointer BulkOperations::AbstractOperation::getConnection() { return m_connection; } int BulkOperations::AbstractOperation::getDbIndex() const { return m_dbIndex; } QRegExp BulkOperations::AbstractOperation::getKeyPattern() const { return m_keyPattern; } void BulkOperations::AbstractOperation::setKeyPattern(const QRegExp p) { m_keyPattern = p; } int BulkOperations::AbstractOperation::currentProgress() const { return m_progress; } void BulkOperations::AbstractOperation::setMetadata(const QVariantMap& meta) { m_metadata = meta; } void BulkOperations::AbstractOperation::incrementProgress() { QMutexLocker l(&m_processedKeysMutex); m_progress++; if (QDateTime::currentMSecsSinceEpoch() - m_lastProgressNotification >= 1000) { qDebug() << "Notify UI about progress"; emit progress(m_progress); m_lastProgressNotification = QDateTime::currentMSecsSinceEpoch(); } } void BulkOperations::AbstractOperation::processError(const QString& err) { { QMutexLocker l(&m_errorsMutex); m_errors.append(m_errorMessagePrefix + err); } m_callback(m_keyPattern, m_progress, m_errors); } ================================================ FILE: src/modules/bulk-operations/operations/abstractoperation.h ================================================ #pragma once #include #include #include #include namespace BulkOperations { class AbstractOperation : public QObject { Q_OBJECT public: enum class State { READY, RUNNING, FINISHED }; typedef std::function OperationCallback; public: AbstractOperation(QSharedPointer connection, int dbIndex, OperationCallback callback, QRegExp keyPattern = QRegExp("*", Qt::CaseSensitive, QRegExp::Wildcard)); virtual ~AbstractOperation() {} virtual void getAffectedKeys(std::function callback); virtual void run(QSharedPointer targetConnection = QSharedPointer(), int targetDbIndex = 0); virtual QString getTypeName() const = 0; virtual bool multiConnectionOperation() const = 0; bool isRunning() const; QSharedPointer getConnection(); int getDbIndex() const; QRegExp getKeyPattern() const; void setKeyPattern(const QRegExp p); int currentProgress() const; void setMetadata(const QVariantMap& meta); signals: void progress(int processed); protected: virtual bool isMetadataValid() const { return true; } virtual void performOperation( QSharedPointer targetConnection, int targetDbIndex) = 0; void incrementProgress(); void processError(const QString& err); protected: QSharedPointer m_connection; int m_dbIndex; QRegExp m_keyPattern; State m_currentState; int m_progress; QList m_affectedKeys; QList m_keysWithErrors; QVariantMap m_metadata; OperationCallback m_callback; QStringList m_errors; QMutex m_errorsMutex; QMutex m_processedKeysMutex; qint64 m_lastProgressNotification; QString m_errorMessagePrefix; }; } // namespace BulkOperations ================================================ FILE: src/modules/bulk-operations/operations/copyoperation.cpp ================================================ #include "copyoperation.h" #include #define RESTORE_BUFFER_LIMIT 100 BulkOperations::CopyOperation::CopyOperation( QSharedPointer connection, int dbIndex, OperationCallback callback, QRegExp keyPattern) : BulkOperations::AbstractOperation(connection, dbIndex, callback, keyPattern) { m_errorMessagePrefix = QCoreApplication::translate("RESP", "Cannot copy key "); } void BulkOperations::CopyOperation::performOperation( QSharedPointer targetConnection, int targetDbIndex) { m_progress = 0; m_dumpedKeys = 0; m_errors.clear(); auto returnResults = [this]() { m_callback(m_keyPattern, m_progress, m_errors); }; if (m_affectedKeys.size() == 0) { return returnResults(); } QByteArray ttl = QString::number(m_metadata["ttl"].toLongLong() * 1000).toUtf8(); QByteArray replace = m_metadata["replace"].toString().toUpper().toUtf8(); auto processKeyDumps = [this, returnResults, ttl, replace, targetConnection, targetDbIndex](const RedisClient::Response& r, QString err) { if (!err.isEmpty()) { return processError(err); } { QMutexLocker l(&m_processedKeysMutex); QVariant incrResult = r.value(); auto getRestoreCmd = [this, r, replace, ttl](const QByteArray& dump) { QList restoreCmd{ "RESTORE", m_affectedKeys[m_dumpedKeys], ttl, dump}; if (!replace.isEmpty()) { restoreCmd.append(replace); } return restoreCmd; }; if (incrResult.canConvert(QVariant::ByteArray)) { m_restoreBuffer.append(getRestoreCmd(incrResult.toByteArray())); m_dumpedKeys++; } else if (incrResult.canConvert(QVariant::List)) { auto responses = incrResult.toList(); for (auto resp : qAsConst(responses)) { m_restoreBuffer.append(getRestoreCmd(resp.toByteArray())); m_dumpedKeys++; } } } if (m_restoreBuffer.size() > RESTORE_BUFFER_LIMIT || m_dumpedKeys == m_affectedKeys.size()) { int batchSize = m_restoreBuffer.size(); targetConnection->pipelinedCmd( m_restoreBuffer, this, targetDbIndex, [this, returnResults, batchSize](const RedisClient::Response& r, QString err) { if (!err.isEmpty() || r.isErrorMessage()) { return processError(err.isEmpty()? r.value().toByteArray() : err); } { QMutexLocker l(&m_processedKeysMutex); m_progress += batchSize; emit progress(m_progress); } if (m_progress >= m_affectedKeys.size()) { returnResults(); } }, false); m_restoreBuffer.clear(); } }; auto processKeys = [this, processKeyDumps]() { QList> rawCmds; for (const QByteArray &k : qAsConst(m_affectedKeys)) { rawCmds.append({"DUMP", k}); } m_connection->pipelinedCmd(rawCmds, this, -1, processKeyDumps, true); }; auto verifySourceConnection = [this, processKeys, targetConnection]() { m_connection->cmd( {"ping"}, this, m_dbIndex, [processKeys, this](const RedisClient::Response& r) { if (r.isErrorMessage()) { return processError( QCoreApplication::translate("RESP", "Source connection error")); } QtConcurrent::run(processKeys); }, [this](const QString& err) { processError(err); }); }; targetConnection->cmd( {"ping"}, this, targetDbIndex, [verifySourceConnection, this](const RedisClient::Response& r) { if (r.isErrorMessage()) { return processError( QCoreApplication::translate("RESP", "Target connection error")); } verifySourceConnection(); }, [this](const QString& err) { processError(err); }); } ================================================ FILE: src/modules/bulk-operations/operations/copyoperation.h ================================================ #pragma once #include #include #include #include #include "abstractoperation.h" namespace BulkOperations { class CopyOperation : public AbstractOperation { Q_OBJECT public: CopyOperation(QSharedPointer connection, int dbIndex, OperationCallback callback, QRegExp keyPattern = QRegExp("*", Qt::CaseSensitive, QRegExp::Wildcard)); QString getTypeName() const override { return QString("copy_keys"); } bool multiConnectionOperation() const override { return true; } bool isMetadataValid() const override { return m_metadata.contains("ttl") && m_metadata.contains("replace"); } protected: void performOperation( QSharedPointer targetConnection, int targetDbIndex) override; private: QList> m_restoreBuffer; int m_dumpedKeys; }; } // namespace BulkOperations ================================================ FILE: src/modules/bulk-operations/operations/deleteoperation.cpp ================================================ #include "deleteoperation.h" #include BulkOperations::DeleteOperation::DeleteOperation( QSharedPointer connection, int dbIndex, OperationCallback callback, QRegExp keyPattern) : BulkOperations::AbstractOperation(connection, dbIndex, callback, keyPattern) { m_errorMessagePrefix = QCoreApplication::translate("RESP", "Cannot remove key "); } void BulkOperations::DeleteOperation::performOperation( QSharedPointer, int) { m_progress = 0; m_errors.clear(); auto returnResults = [this]() { qDebug() << "Processed keys: " << m_progress; m_callback(m_keyPattern, m_progress, m_errors); }; if (m_affectedKeys.size() == 0) { return returnResults(); } AsyncFuture::observe(m_connection->isCommandSupported({"UNLINK"})) .subscribe([this, returnResults](bool supportUnlink) { QByteArray removalCmd{"DEL"}; if (supportUnlink) { removalCmd = "UNLINK"; } QtConcurrent::run(this, &DeleteOperation::deleteKeys, m_affectedKeys, removalCmd, [this, removalCmd, returnResults]() { // Retry on keys with errors if (m_keysWithErrors.size() > 0) { m_errors.clear(); deleteKeys(m_keysWithErrors, removalCmd, returnResults); } else { returnResults(); } }); }); } void BulkOperations::DeleteOperation::deleteKeys( const QList &keys, const QByteArray &rmCmd, std::function callback) { QList> rawCmds; for (const QByteArray& k : keys) { rawCmds.append({rmCmd, k}); } int batchSize = m_connection->pipelineCommandsLimit(); int expectedResponses = rawCmds.size(); m_connection->pipelinedCmd( rawCmds, this, m_dbIndex, [this, expectedResponses, callback, batchSize]( const RedisClient::Response &r, QString err) { if (!err.isEmpty() || r.isErrorMessage()) { return processError(err.isEmpty() ? r.value().toByteArray() : err); } { QMutexLocker l(&m_processedKeysMutex); m_progress += batchSize; emit progress(m_progress); } if (m_progress >= expectedResponses) { callback(); } }, false); } ================================================ FILE: src/modules/bulk-operations/operations/deleteoperation.h ================================================ #pragma once #include #include #include #include #include "abstractoperation.h" namespace BulkOperations { class DeleteOperation : public AbstractOperation { Q_OBJECT public: DeleteOperation(QSharedPointer connection, int dbIndex, OperationCallback callback, QRegExp keyPattern = QRegExp("*", Qt::CaseSensitive, QRegExp::Wildcard)); QString getTypeName() const override { return QString("delete_keys"); } bool multiConnectionOperation() const override { return false; } protected: void performOperation( QSharedPointer targetConnection, int targetDbIndex) override; void deleteKeys(const QList &keys, const QByteArray& rmCmd, std::function callback); }; } // namespace BulkOperations ================================================ FILE: src/modules/bulk-operations/operations/rdbimport.cpp ================================================ #include "rdbimport.h" #include #include #include #include BulkOperations::RDBImportOperation::RDBImportOperation( QSharedPointer connection, int dbIndex, OperationCallback callback, QSharedPointer p, QRegExp keyPattern) : BulkOperations::AbstractOperation(connection, dbIndex, callback, keyPattern), m_python(p) { m_python->importModule_sync("rdb"); m_errorMessagePrefix = QCoreApplication::translate("RESP", "Cannot execute command "); } void BulkOperations::RDBImportOperation::getAffectedKeys( std::function callback) { m_keyPattern.setPatternSyntax(QRegExp::RegExp2); if (!m_keyPattern.isValid()) { return callback(QVariant(), QCoreApplication::translate( "RESP", "Invalid regexp for keys filter.")); } m_python->call_native( "rdb.rdb_list_keys", QVariantList{m_metadata["path"].toString(), m_metadata["db"].toInt(), m_keyPattern.pattern()}, [callback, this](QVariant v) { m_affectedKeys.clear(); if (v.isNull()) { return callback(QVariant(), QCoreApplication::translate( "RESP", "Cannot get the list of affected keys")); } QVariantList keys = v.toList(); QStringList keyNames; for (const QVariant &k : qAsConst(keys)) { m_affectedKeys.append(k.toByteArray()); keyNames.append(printableString(k.toByteArray(), true)); } return callback(QVariant(keyNames), ""); }); } bool BulkOperations::RDBImportOperation::isMetadataValid() const { return m_metadata.contains("db") && m_metadata.contains("path") && QFileInfo::exists(m_metadata["path"].toString()); } QList convertToByteArray(QVariant v) { QVariantList l = v.toList(); QList result; for (const QVariant &b : qAsConst(l)) { result.append(b.toByteArray()); } return result; } void BulkOperations::RDBImportOperation::performOperation( QSharedPointer, int) { m_progress = 0; m_errors.clear(); auto returnResults = [this]() { m_callback(m_keyPattern, m_progress, m_errors); }; if (m_affectedKeys.size() == 0) { return returnResults(); } auto processCommands = [this, returnResults](const QVariantList& commands) { QList> rawCmds; for (const QVariant &cmd : commands) { auto rawCmd = convertToByteArray(cmd); if (rawCmd.at(0).toLower() == QByteArray("select")) { continue; } rawCmds.append(rawCmd); } int batchSize = m_connection->pipelineCommandsLimit(); int expectedResponses = rawCmds.size(); m_connection->pipelinedCmd( rawCmds, this, m_dbIndex, [this, returnResults, expectedResponses, batchSize](const RedisClient::Response& r, const QString& err) { if (!err.isEmpty() || r.isErrorMessage()) { return processError(err.isEmpty()? r.value().toByteArray() : err); } { QMutexLocker l(&m_processedKeysMutex); m_progress += batchSize; emit progress(m_progress); } if (m_progress >= expectedResponses) { returnResults(); } }, false); }; m_python->call_native( "rdb.rdb_export_as_commands", QVariantList{m_metadata["path"].toString(), m_metadata["db"].toInt(), m_keyPattern.pattern()}, [processCommands, this](QVariant v) { QVariantList commands = v.toList(); m_connection->cmd( {"ping"}, this, m_dbIndex, [processCommands, commands, this](const RedisClient::Response& r) { if (r.isErrorMessage()) { return processError(QCoreApplication::translate( "RESP", "Target connection error")); } QtConcurrent::run(processCommands, commands); }, [this](const QString& err) { processError(err); }); }); } ================================================ FILE: src/modules/bulk-operations/operations/rdbimport.h ================================================ #pragma once #include #include #include #include #include "abstractoperation.h" class QPython; namespace BulkOperations { class RDBImportOperation : public AbstractOperation { Q_OBJECT public: RDBImportOperation(QSharedPointer connection, int dbIndex, OperationCallback callback, QSharedPointer p, QRegExp keyPattern = QRegExp("*", Qt::CaseSensitive, QRegExp::Wildcard)); QString getTypeName() const override { return QString("rdb_import"); } bool multiConnectionOperation() const override { return false; } void getAffectedKeys( std::function callback) override; bool isMetadataValid() const override; protected: void performOperation( QSharedPointer targetConnection, int targetDbIndex) override; private: QSharedPointer m_python; }; } // namespace BulkOperations ================================================ FILE: src/modules/bulk-operations/operations/ttloperation.cpp ================================================ #include "ttloperation.h" #include BulkOperations::TtlOperation::TtlOperation( QSharedPointer connection, int dbIndex, OperationCallback callback, QRegExp keyPattern) : BulkOperations::AbstractOperation(connection, dbIndex, callback, keyPattern) { m_errorMessagePrefix = QCoreApplication::translate("RESP", "Cannot set TTL for key "); } void BulkOperations::TtlOperation::performOperation( QSharedPointer, int) { m_progress = 0; m_errors.clear(); auto returnResults = [this]() { m_callback(m_keyPattern, m_progress, m_errors); }; if (m_affectedKeys.size() == 0) { return returnResults(); } QByteArray ttl = m_metadata["ttl"].toString().toUtf8(); QtConcurrent::run(this, &TtlOperation::setTtl, m_affectedKeys, ttl, [this, ttl, returnResults]() { // Retry on keys with errors if (m_keysWithErrors.size() > 0) { m_errors.clear(); setTtl(m_keysWithErrors, ttl, returnResults); } else { returnResults(); } }); } void BulkOperations::TtlOperation::setTtl(const QList& keys, const QByteArray& ttl, std::function callback) { QList> rawCmds; for (const QByteArray& k : keys) { rawCmds.append({"EXPIRE", k, ttl}); } int batchSize = m_connection->pipelineCommandsLimit(); int expectedResponses = rawCmds.size(); m_connection->pipelinedCmd( rawCmds, this, -1, [this, expectedResponses, callback, batchSize]( const RedisClient::Response& r, QString err) { if (!err.isEmpty() || r.isErrorMessage()) { return processError(err.isEmpty() ? r.value().toByteArray() : err); } { QMutexLocker l(&m_processedKeysMutex); m_progress += batchSize; emit progress(m_progress); } if (m_progress >= expectedResponses) { callback(); } }, false); } ================================================ FILE: src/modules/bulk-operations/operations/ttloperation.h ================================================ #pragma once #include #include #include #include #include "abstractoperation.h" namespace BulkOperations { class TtlOperation : public AbstractOperation { Q_OBJECT public: TtlOperation(QSharedPointer connection, int dbIndex, OperationCallback callback, QRegExp keyPattern = QRegExp("*", Qt::CaseSensitive, QRegExp::Wildcard)); QString getTypeName() const override { return QString("ttl"); } bool multiConnectionOperation() const override { return false; } bool isMetadataValid() const override { return m_metadata.contains("ttl"); } protected: void performOperation( QSharedPointer targetConnection, int targetDbIndex) override; void setTtl(const QList &keys, const QByteArray &ttl, std::function callback); }; } // namespace BulkOperations ================================================ FILE: src/modules/common/baselistmodel.cpp ================================================ #include "baselistmodel.h" BaseListModel::BaseListModel(QObject *parent) : QAbstractListModel(parent) { } QVariantMap BaseListModel::getRowRaw(int row) { QHash names = roleNames(); QHashIterator i(names); QVariantMap res; while (i.hasNext()) { i.next(); if (i.value() == "display") continue; QModelIndex idx = index(row); QVariant d = data(idx, i.key()); res[i.value()] = d; } return res; } ================================================ FILE: src/modules/common/baselistmodel.h ================================================ #pragma once #include class BaseListModel : public QAbstractListModel { Q_OBJECT public: BaseListModel(QObject *parent = Q_NULLPTR); virtual ~BaseListModel() {} protected: QVariantMap getRowRaw(int row); inline bool isIndexValid(const QModelIndex &index) const { return 0 <= index.row() && index.row() < rowCount(); } inline bool isRowIndexValid(int row) const { return 0 <= row && row < rowCount(); } }; ================================================ FILE: src/modules/common/callbackwithowner.h ================================================ #pragma once #include #include #include #include #include template class CallbackWithOwner { public: CallbackWithOwner(QWeakPointer owner, std::function c) : m_owner(owner), m_callback(c) { } void call(Args... args) { if (!isValid()) { qDebug() << "Callback owner was destroyed"; return; } return m_callback(args...); } bool isValid() { auto owner = m_owner.toStrongRef(); return !owner.isNull(); } std::function rawCallback() { return m_callback; } private: QWeakPointer m_owner; std::function m_callback; }; ================================================ FILE: src/modules/common/sortfilterproxymodel.cpp ================================================ #include "sortfilterproxymodel.h" #include "sortfilterproxymodel.h" #include #include SortFilterProxyModel::SortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent), m_complete(false) { } QObject *SortFilterProxyModel::source() const { return sourceModel(); } void SortFilterProxyModel::setSource(QObject *source) { auto m = qobject_cast(source); if (!m) return; setSourceModel(m); } QByteArray SortFilterProxyModel::sortRole() const { return m_sortRole; } void SortFilterProxyModel::setSortRole(const QByteArray &role) { if (m_sortRole != role) { m_sortRole = role; if (m_complete) QSortFilterProxyModel::setSortRole(roleKey(role)); } } void SortFilterProxyModel::setSortOrder(Qt::SortOrder order) { QSortFilterProxyModel::sort(0, order); } QByteArray SortFilterProxyModel::filterRole() const { return m_filterRole; } void SortFilterProxyModel::setFilterRole(const QByteArray &role) { if (m_filterRole != role) { m_filterRole = role; if (m_complete) QSortFilterProxyModel::setFilterRole(roleKey(role)); } } QString SortFilterProxyModel::filterString() const { return filterRegExp().pattern(); } void SortFilterProxyModel::setFilterString(const QString &filter) { setFilterRegExp(QRegExp(filter, filterCaseSensitivity(), static_cast(filterSyntax()))); emit filterStringChanged(); } SortFilterProxyModel::FilterSyntax SortFilterProxyModel::filterSyntax() const { return static_cast(filterRegExp().patternSyntax()); } void SortFilterProxyModel::setFilterSyntax(SortFilterProxyModel::FilterSyntax syntax) { setFilterRegExp(QRegExp(filterString(), filterCaseSensitivity(), static_cast(syntax))); } void SortFilterProxyModel::classBegin() { } void SortFilterProxyModel::componentComplete() { m_complete = true; if (!m_sortRole.isEmpty()) QSortFilterProxyModel::setSortRole(roleKey(m_sortRole)); if (!m_filterRole.isEmpty()) QSortFilterProxyModel::setFilterRole(roleKey(m_filterRole)); } int SortFilterProxyModel::getOriginalRowIndex(int i) { QModelIndex proxyIndex = index(i, 0); return mapToSource(proxyIndex).row(); } int SortFilterProxyModel::roleKey(const QByteArray &role) const { QHash roles = roleNames(); QHashIterator it(roles); while (it.hasNext()) { it.next(); if (it.value() == role) return it.key(); } return -1; } ================================================ FILE: src/modules/common/sortfilterproxymodel.h ================================================ #pragma once #include #include class SortFilterProxyModel : public QSortFilterProxyModel, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(QObject *source READ source WRITE setSource) Q_PROPERTY(QByteArray sortRole READ sortRole WRITE setSortRole) Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder) Q_PROPERTY(QByteArray filterRole READ filterRole WRITE setFilterRole) Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged) Q_PROPERTY(FilterSyntax filterSyntax READ filterSyntax WRITE setFilterSyntax) Q_PROPERTY(int filterKeyColumn READ filterKeyColumn WRITE setFilterKeyColumn) Q_ENUMS(FilterSyntax) public: explicit SortFilterProxyModel(QObject *parent = 0); QObject *source() const; void setSource(QObject *source); QByteArray sortRole() const; Q_INVOKABLE void setSortRole(const QByteArray &role); Q_INVOKABLE void setSortOrder(Qt::SortOrder order); QByteArray filterRole() const; void setFilterRole(const QByteArray &role); QString filterString() const; void setFilterString(const QString &filter); enum FilterSyntax { RegExp, Wildcard, FixedString }; FilterSyntax filterSyntax() const; void setFilterSyntax(FilterSyntax syntax); void classBegin(); void componentComplete(); Q_INVOKABLE int getOriginalRowIndex(int i); signals: void filterStringChanged(); protected: int roleKey(const QByteArray &role) const; private: bool m_complete; QByteArray m_sortRole; QByteArray m_filterRole; }; ================================================ FILE: src/modules/common/tabmodel.cpp ================================================ #include "tabmodel.h" #include TabModel::TabModel(QSharedPointer connection, int dbIndex) : m_dbIndex(dbIndex) { m_connection = connection->clone(); } TabModel::~TabModel() { QtConcurrent::run( [](QSharedPointer connection) { connection->disconnect(); }, m_connection); m_connection.clear(); } void TabModel::init() { auto weekPointer = sharedFromThis().toWeakRef(); m_connection->callAfterConnect([this, weekPointer](const QString& err) { if (!weekPointer) { return; } if (!err.isEmpty()) { emit error(err); return; } if (m_dbIndex) { m_connection->command({"PING"}, m_dbIndex); } emit initialized(); }); try { m_connection->connect(false); } catch (RedisClient::Connection::Exception&) { emit error(QCoreApplication::translate( "RESP", "Invalid Connection. Check connection settings.")); return; } } QSharedPointer TabModel::getConnection() const { return m_connection; } ================================================ FILE: src/modules/common/tabmodel.h ================================================ #pragma once #include #include #include class TabModel : public QObject, public QEnableSharedFromThis { Q_OBJECT public: TabModel(QSharedPointer connection, int dbIndex); virtual ~TabModel(); virtual QString getName() const = 0; Q_INVOKABLE virtual void init(); virtual QSharedPointer getConnection() const; signals: void error(const QString& error); void initialized(); protected: QSharedPointer m_connection; uint m_dbIndex; }; ================================================ FILE: src/modules/common/tabviewmodel.cpp ================================================ #include "tabviewmodel.h" #include TabViewModel::TabViewModel(const ModelFactory &modelFactory) : m_modelFactory(modelFactory) {} QModelIndex TabViewModel::index(int row, int, const QModelIndex &) const { return createIndex(row, 0); } int TabViewModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_models.count(); } QVariant TabViewModel::data(const QModelIndex &index, int role) const { if (!isIndexValid(index)) return QVariant(); QSharedPointer model = m_models.at(index.row()); if (model.isNull()) return QVariant(); switch (role) { case tabIndex: return index.row(); case tabName: return model->getName(); case tabModel: QObject *modelPtr = static_cast(m_models.at(index.row()).data()); QQmlEngine::setObjectOwnership(modelPtr, QQmlEngine::CppOwnership); return QVariant::fromValue(modelPtr); } return QVariant(); } QHash TabViewModel::roleNames() const { QHash roles; roles[tabIndex] = "tabIndex"; roles[tabName] = "tabName"; roles[tabModel] = "tabModel"; return roles; } void TabViewModel::closeTab(int i) { if (!isIndexValid(index(i, 0))) return; beginRemoveRows(QModelIndex(), i, i); m_models.at(i)->disconnect(); m_models.removeAt(i); endRemoveRows(); } int TabViewModel::tabsCount() const { return m_models.count(); } void TabViewModel::openTab(QSharedPointer connection, int dbIndex, bool inNewTab, QList initCmd) { if (inNewTab) { beginInsertRows(QModelIndex(), m_models.count(), m_models.count()); m_models.append(m_modelFactory(connection, dbIndex, initCmd)); emit changeCurrentTab(m_models.size() - 1); endInsertRows(); } else { bool found = false; for (int index = 0; 0 <= index && index < m_models.size(); index++) { auto model = m_models.at(index); if (model->getConnection()->getConfig().id() == connection->getConfig().id()) { found = true; emit changeCurrentTab(index); break; } } if (!found) { return openTab(connection, dbIndex, true, initCmd); } } } void TabViewModel::closeAllTabsWithConnection( QSharedPointer connection) { for (int index = 0; 0 <= index && index < m_models.size(); index++) { auto model = m_models.at(index); if (model->getConnection()->getConfig().id() == connection->getConfig().id()) { beginRemoveRows(QModelIndex(), index, index); m_models.removeAt(index); endRemoveRows(); index--; } } } bool TabViewModel::isIndexValid(const QModelIndex &index) const { return 0 <= index.row() && index.row() < rowCount(); } ================================================ FILE: src/modules/common/tabviewmodel.h ================================================ #pragma once #include #include #include "tabmodel.h" class TabViewModel : public QAbstractListModel { Q_OBJECT public: enum Roles { tabName = Qt::UserRole + 1, tabIndex, tabModel, }; typedef std::function( QSharedPointer, int dbIndex, QList initCmd)> ModelFactory; public: TabViewModel(const ModelFactory& modelFactory); QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; public: // methods exported to QML Q_INVOKABLE void closeTab(int i); Q_INVOKABLE int tabsCount() const; signals: void changeCurrentTab(int i); public slots: virtual void openTab(QSharedPointer connection, int dbIndex=0, bool inNewTab=true, QList initCmd = QList()); void closeAllTabsWithConnection( QSharedPointer connection); private: QList> m_models; ModelFactory m_modelFactory; bool isIndexValid(const QModelIndex& index) const; }; template TabViewModel::ModelFactory getTabModelFactory() { return TabViewModel::ModelFactory( [](QSharedPointer c, int dbIndex, QList initCmd) { return QSharedPointer(new T(c, dbIndex, initCmd), &QObject::deleteLater); }); } ================================================ FILE: src/modules/connections-tree/items/abstractnamespaceitem.cpp ================================================ #include "abstractnamespaceitem.h" #include #include #include #include #include #include "connections-tree/keysrendering.h" #include "connections-tree/model.h" #include "connections-tree/operations.h" #include "keyitem.h" #include "loadmoreitem.h" #include "namespaceitem.h" using namespace ConnectionsTree; AbstractNamespaceItem::AbstractNamespaceItem( Model &model, QWeakPointer parent, QSharedPointer operations, uint dbIndex, QRegExp filter) : TreeItem(model), m_parent(parent), m_operations(operations), m_filter(filter.isEmpty() ? QRegExp(operations->defaultFilter()) : filter), m_dbIndex(dbIndex), m_runningOperation(nullptr) { QSettings settings; m_showNsOnTop = settings .value("app/showNamespacesOnTop", #if defined(Q_OS_WINDOWS) true #else false #endif ) .toBool(); } QList> AbstractNamespaceItem::getAllChilds() const { return m_childItems; } QList> AbstractNamespaceItem::getAllChildNamespaces() const { return m_childNamespaces.values(); } QSharedPointer AbstractNamespaceItem::child(uint row) { if (row < m_childItems.size()) return m_childItems.at(row); return QSharedPointer(); } QWeakPointer AbstractNamespaceItem::parent() const { return m_parent; } void AbstractNamespaceItem::append(QSharedPointer item, bool notifyModel) { if (notifyModel) m_model.beforeChildLoadedAtPos(getSelf(), m_childItems.size()); m_childItems.append(item); if (notifyModel) m_model.childLoaded(getSelf()); } bool compareTreeItemsByName(QSharedPointer first, QSharedPointer second) { return first->getDisplayName() < second->getDisplayName(); } bool compareTreeItemsByNameAndNsOnTop(QSharedPointer first, QSharedPointer second) { if (first->type() != second->type()) return first->type() > second->type(); return first->getDisplayName() < second->getDisplayName(); } void AbstractNamespaceItem::insertChild(QSharedPointer item) { auto pos = std::upper_bound(m_childItems.begin(), m_childItems.end(), item, m_showNsOnTop ? compareTreeItemsByNameAndNsOnTop : compareTreeItemsByName); int index = std::distance(m_childItems.begin(), pos); m_model.beforeChildLoadedAtPos(getSelf(), index); m_childItems.insert(pos, item); m_model.childLoaded(getSelf()); } void AbstractNamespaceItem::appendKeyToIndex(QSharedPointer key) { if (!key) return; m_keysIndex.insert(key->getFullPath(), key.toWeakRef()); } void AbstractNamespaceItem::removeNamespacedKeysFromIndex(QByteArray nsPrefix) { auto keys = m_keysIndex.keys(); for (auto const &fullPath : qAsConst(keys)) { if (fullPath.startsWith(nsPrefix)) { m_keysIndex.remove(fullPath); } } } QHash> AbstractNamespaceItem::getKeysIndex() const { return m_keysIndex; } QSharedPointer resolveItemToRemove(QSharedPointer item) { if (!item) return item; auto parent = item->parent().toStrongRef(); if (!parent || parent->type() == "database") return item; if (parent->type() == "namespace" && parent->getAllChilds().empty()) return resolveItemToRemove(parent); return item; } void AbstractNamespaceItem::removeObsoleteKeys( QList> keys) { for (const auto &obsoleteKey : keys) { auto key = obsoleteKey.toStrongRef(); if (!key) continue; m_keysIndex.remove(key->getFullPath()); auto parent = key->parent().toStrongRef(); if (!parent) continue; QSharedPointer itemToRemoveFromModel = key; if (parent->type() == "namespace" && parent->getAllChilds().size() == 1) { itemToRemoveFromModel = resolveItemToRemove(parent); } if (!itemToRemoveFromModel) continue; int row = itemToRemoveFromModel->row(); m_model.beforeItemChildRemoved(itemToRemoveFromModel->parent(), row); auto parentHoldsItemToRemove = itemToRemoveFromModel->parent().toStrongRef(); if (!parentHoldsItemToRemove) continue; parentHoldsItemToRemove->removeChild(row); m_model.itemChildRemoved(itemToRemoveFromModel); } } void AbstractNamespaceItem::removeChild(int index) { bool validIndex = 0 < index && index < m_childItems.size(); if (!validIndex) return; m_childItems.removeAt(index); } void AbstractNamespaceItem::appendRawKey(const QByteArray &k) { m_rawChildKeys.append(k); } void AbstractNamespaceItem::appendNamespace( QSharedPointer item) { m_childNamespaces[item->getName()] = item; insertChild(item.staticCast()); } uint AbstractNamespaceItem::childCount(bool recursive) const { uint count = 0; if (!recursive) { count += m_childItems.size(); return count; } for (const auto &item : m_childItems) { if (item->supportChildItems()) { count += item->childCount(true); } else { count += 1; } } return count; } uint AbstractNamespaceItem::keysCount() const { uint count = m_rawChildKeys.size(); for (const auto &item : m_childItems) { if (item->supportChildItems()) { auto ns = item.dynamicCast(); if (ns) { count += ns->keysCount(); } } else if (item->type() == "key") { count += 1; } } return count; } uint AbstractNamespaceItem::keysRenderingLimit() const { QSettings appSettings; return appSettings.value("app/treeItemMaxChilds", 1000).toUInt(); } bool AbstractNamespaceItem::keysShortNameRendering() const { QSettings appSettings; return appSettings.value("app/namespacedKeysShortName", true).toBool(); } void AbstractNamespaceItem::clear() { clearLoader(); bool notifyModel = false; if (m_childItems.size() > 0) { notifyModel = true; m_model.beforeItemChildsUnloaded(getSelf()); } m_childItems.clear(); m_childNamespaces.clear(); m_rawChildKeys.clear(); m_usedMemory = 0; if (type() == "database") { m_keysIndex.clear(); } else { auto selfRef = getSelf().toStrongRef().dynamicCast(); if (selfRef) { auto root = resolveRootItem(selfRef); if (root) { root->removeNamespacedKeysFromIndex(getFullPath()); } } } if (notifyModel) { m_model.itemChildRemoved(getSelf()); } } void AbstractNamespaceItem::clearLoader() { if (m_childItems.empty()) { return; } auto lastItem = m_childItems.last(); if (!lastItem || lastItem->type() != "loader") return; m_model.beforeItemChildRemoved(getSelf(), m_childItems.size() - 1); m_childItems.removeLast(); m_model.itemChildRemoved(lastItem.toWeakRef()); } void AbstractNamespaceItem::showLoadingError(const QString &err) { emit m_model.itemChanged(getSelf()); emit m_model.error(err); } void AbstractNamespaceItem::cancelCurrentOperation() { if (m_runningOperation) { m_runningOperation->future().cancel(); m_operations->resetConnection(); unlock(); } } bool compareChilds(QSharedPointer first, QSharedPointer second) { auto firstMemoryItem = first.dynamicCast(); auto secondMemoryItem = second.dynamicCast(); if (!firstMemoryItem) qDebug() << "Invalid tree item:" << first->getDisplayName(); if (!secondMemoryItem) qDebug() << "Invalid tree item:" << second->getDisplayName(); return (firstMemoryItem ? firstMemoryItem->usedMemory() : 0) > (secondMemoryItem ? secondMemoryItem->usedMemory() : 0); } void AbstractNamespaceItem::sortChilds() { m_model.beforeItemLayoutChanged(getSelf()); std::sort(m_childItems.begin(), m_childItems.end(), compareChilds); m_model.itemLayoutChanged(getSelf()); emit m_model.itemChanged(getSelf()); } void AbstractNamespaceItem::renderRawKeys( const RedisClient::Connection::RawKeysList &keylist, QRegExp filter, QSharedPointer callback, bool appendNewItems, bool checkPreRenderedItems, int maxChildItems) { if (!m_operations) { return; } uint renderingLimit = qMax(static_cast(m_childItems.size()), keysRenderingLimit()); if (maxChildItems > 0) { renderingLimit = static_cast(maxChildItems); } auto settings = ConnectionsTree::KeysTreeRenderer::RenderingSettigns{ filter, m_operations->getNamespaceSeparator(), getDbIndex(), renderingLimit, appendNewItems, checkPreRenderedItems, keysShortNameRendering()}; auto future = QtConcurrent::run( [](QList keylist) { std::sort(keylist.begin(), keylist.end()); return keylist; }, keylist); auto selfWPtr = getSelf(); AsyncFuture::observe(future).subscribe([selfWPtr, this, settings, callback, future]() { auto self = selfWPtr.toStrongRef(); if (!self) return; ConnectionsTree::KeysTreeRenderer::renderKeys( m_operations, future.result(), qSharedPointerDynamicCast(self), settings, m_model.expandedNamespaces); if (callback) callback->call(); }); } void AbstractNamespaceItem::ensureLoaderIsCreated() { if (m_rawChildKeys.empty() || m_childItems.empty()) { return; } auto lastItem = m_childItems.last(); if (lastItem->type() == "loader") return; m_model.beforeChildLoaded(getSelf(), 1); m_childItems.append( QSharedPointer(new LoadMoreItem(getSelf(), m_model))); m_model.childLoaded(getSelf()); } QHash > AbstractNamespaceItem::eventHandlers() { auto events = TreeItem::eventHandlers(); events.insert("analyze_memory_usage", [this]() { if (m_usedMemory > 0) return true; auto future = m_operations->connectionSupportsMemoryOperations(); auto selfWPtr = getSelf(); AsyncFuture::observe(future).subscribe([selfWPtr, this](bool isSupported) { auto self = selfWPtr.toStrongRef(); if (!self) return; if (!isSupported) { emit m_model.error(QCoreApplication::translate( "RESP", "Your redis-server doesn't support MEMORY " "commands.")); unlock(); return; } getMemoryUsage([selfWPtr, this](qlonglong) { QTimer::singleShot(0, this, [this]() { sortChilds(); unlock(); m_runningOperation.clear(); }); }); }); return false; }); return events; } void AbstractNamespaceItem::getMemoryUsage( std::function callback) { m_usedMemory = 0; m_runningOperation = QSharedPointer>( new AsyncFuture::Deferred()); QtConcurrent::run(this, &AbstractNamespaceItem::calculateUsedMemory, m_runningOperation, callback); return; } void AbstractNamespaceItem::fetchMore() { if (m_rawChildKeys.empty()) { return; } clearLoader(); int childsCount = m_childItems.size(); auto rawKeys = m_rawChildKeys; emit m_model.itemChanged(getSelf()); m_rawChildKeys.clear(); auto callback = QSharedPointer( new RenderRawKeysCallback(getSelf(), [this]() { ensureLoaderIsCreated(); unlock(); })); return renderRawKeys( rawKeys, m_filter, callback, true, false, static_cast(childsCount) + keysRenderingLimit()); } void AbstractNamespaceItem::calculateUsedMemory( QSharedPointer> parentDeffered, std::function callback) { if (parentDeffered && parentDeffered->future().isCanceled()) { return; } if (m_rawChildKeys.size() > 0) { auto resultCallback = QSharedPointer( new Operations::GetUsedMemoryCallback( getSelf(), [this, callback](qlonglong result) { m_usedMemory = result; emit m_model.itemChanged(getSelf()); if (m_childItems.empty()) callback(result); })); auto progressCallback = QSharedPointer( new Operations::GetUsedMemoryCallback( getSelf(), [this](qlonglong progress) { m_usedMemory = progress; emit m_model.itemChanged(getSelf()); })); operations()->getUsedMemory(m_rawChildKeys, m_dbIndex, resultCallback, progressCallback); } auto resultsRemaining = QSharedPointer(new qlonglong(0)); auto updateUsedMemoryValue = [this, resultsRemaining, callback](qlonglong result) { if (!resultsRemaining) return; QMutexLocker locker(&m_updateUsedMemoryMutex); Q_UNUSED(locker); m_usedMemory += result; emit m_model.itemChanged(getSelf()); (*resultsRemaining)--; if (*resultsRemaining <= 0) { callback(m_usedMemory); } }; (*resultsRemaining) += m_childNamespaces.size(); for (auto childNs : qAsConst(m_childNamespaces)) { if (parentDeffered->future().isCanceled()) { return; } childNs->calculateUsedMemory(parentDeffered, updateUsedMemoryValue); } for (const QSharedPointer &child : qAsConst(m_childItems)) { if (parentDeffered->future().isCanceled()) { return; } if (!child || child->type() != "key") continue; auto memoryItem = child.dynamicCast(); if (!memoryItem) continue; QMutexLocker locker(&m_updateUsedMemoryMutex); (*resultsRemaining)++; memoryItem->getMemoryUsage(updateUsedMemoryValue); } } void AbstractNamespaceItem::restoreOpenedNamespaces(QSharedPointer ns) { if (ns->type() == "namespace" && !ns->isExpanded()) return; if (ns->isExpanded()) m_model.expandItem(ns.staticCast().toWeakRef()); auto childs = ns->getAllChildNamespaces(); for (auto childNs : childs) { restoreOpenedNamespaces(childNs); } } ================================================ FILE: src/modules/connections-tree/items/abstractnamespaceitem.h ================================================ #pragma once #include #include #include #include #include #include "memoryusage.h" #include "treeitem.h" #include "modules/common/callbackwithowner.h" namespace ConnectionsTree { class Operations; class AbstractNamespaceItem; class Model; class KeyItem; class AbstractNamespaceItem : public QObject, public TreeItem, public MemoryUsage { public: AbstractNamespaceItem(Model& model, QWeakPointer parent, QSharedPointer operations, uint dbIndex, QRegExp filter = QRegExp()); virtual ~AbstractNamespaceItem() {} QList> getAllChilds() const override; QList> getAllChildNamespaces() const; uint childCount(bool recursive = false) const override; uint keysCount() const; uint keysRenderingLimit() const; bool keysShortNameRendering() const; QSharedPointer child(uint row) override; QWeakPointer parent() const override; virtual void append(QSharedPointer item, bool notifyModel=true); virtual void insertChild(QSharedPointer item); virtual void appendKeyToIndex(QSharedPointer key); virtual void removeNamespacedKeysFromIndex(QByteArray nsPrefix); virtual QHash> getKeysIndex() const; virtual void removeObsoleteKeys(QList> keys); void removeChild(int index) override; virtual void appendRawKey(const QByteArray& k); virtual void appendNamespace(QSharedPointer item); virtual QSharedPointer findChildNamespace( const QByteArray& name) { if (!m_childNamespaces.contains(name)) return QSharedPointer(); return m_childNamespaces[name]; } virtual uint getDbIndex() { return m_dbIndex; } virtual QSharedPointer operations() { return m_operations; } virtual QRegExp getFilter() const { return m_filter; } virtual void showLoadingError(const QString& err); void cancelCurrentOperation() override; void getMemoryUsage(std::function) override; void fetchMore() override; protected: virtual void clear(); virtual void ensureLoaderIsCreated(); virtual void clearLoader(); void sortChilds(); using RenderRawKeysCallback = CallbackWithOwner; void renderRawKeys(const RedisClient::Connection::RawKeysList& keylist, QRegExp filter, QSharedPointer callback, bool appendNewItems, bool checkPreRenderedItems, int maxChildItems=-1); QHash> eventHandlers() override; void calculateUsedMemory(QSharedPointer> parentD, std::function callback); void restoreOpenedNamespaces(QSharedPointer ns); protected: QWeakPointer m_parent; QSharedPointer m_operations; QList> m_childItems; QHash> m_childNamespaces; QList m_rawChildKeys; QRegExp m_filter; uint m_dbIndex; QSharedPointer> m_runningOperation; bool m_showNsOnTop; QHash> m_keysIndex; }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/items/databaseitem.cpp ================================================ #include "databaseitem.h" #include #include #include #include #include #include #include #include #include "app/apputils.h" #include "connections-tree/model.h" #include "connections-tree/utils.h" #include "keyitem.h" #include "loadmoreitem.h" #include "namespaceitem.h" #include "serveritem.h" using namespace ConnectionsTree; DatabaseItem::DatabaseItem(unsigned int index, int keysCount, QSharedPointer operations, QWeakPointer parent, Model& model) : AbstractNamespaceItem(model, parent, operations, index), m_keysCount(keysCount) {} DatabaseItem::~DatabaseItem() {} QByteArray DatabaseItem::getName() const { return QByteArray(); } QByteArray DatabaseItem::getFullPath() const { return QByteArray(); } QString DatabaseItem::getDisplayName() const { QString filter = m_filter.pattern() == "*" ? "" : QString("[filter: %1]").arg(m_filter.pattern()); QString baseString = QString("db%1").arg(m_dbIndex); if (m_usedMemory > 0) { baseString.append( QString(" [%1]").arg(humanReadableSize(m_usedMemory))); } if (m_operations->mode() == "cluster") { return QString("%1 %2").arg(baseString).arg(filter); } else { return QString("%1 %2 (%3)").arg(baseString).arg(filter).arg(m_keysCount); } } bool DatabaseItem::isEnabled() const { return true; } void DatabaseItem::loadKeys(std::function callback, bool partialReload) { lock(); QString filter = (m_filter.isEmpty()) ? "" : m_filter.pattern(); auto self = getSelf().toStrongRef(); if (!self) { unlock(); return; } auto dbLoadCallback = QSharedPointer( new Operations::GetDatabasesCallback( getSelf(), [this](QMap dbMapping, const QString& err) { if (err.size() > 0) { unlock(); emit m_model.error(QCoreApplication::translate( "RESP", "Cannot load databases:\n\n") + err); return; } if (dbMapping.contains(m_dbIndex)) { m_keysCount = dbMapping[m_dbIndex]; emit m_model.itemChanged(getSelf()); } })); m_operations->getDatabases(dbLoadCallback); auto onKeysRendered = QSharedPointer( new RenderRawKeysCallback(getSelf(), [this, callback]() { ensureLoaderIsCreated(); unlock(); if (!isExpanded()) { setExpanded(true); m_model.expandItem(getSelf()); } emit m_model.itemChanged(getSelf()); if (callback) { callback(); } })); auto nsItemsCallback = QSharedPointer( new Operations::LoadNamespaceItemsCallback( getSelf(), [this, onKeysRendered, partialReload]( const RedisClient::Connection::RawKeysList& keylist, const QString& err) { if (!err.isEmpty()) { unlock(); return showLoadingError(err); } return renderRawKeys(keylist, m_filter, onKeysRendered, !partialReload, partialReload); })); m_operations->loadNamespaceItems(m_dbIndex, filter, nsItemsCallback); } QVariantMap DatabaseItem::metadata() const { QVariantMap metadata = TreeItem::metadata(); metadata["filter"] = m_filter.pattern(); metadata["filterHistory"] = filterHistoryTop10(); metadata["live_update"] = isLiveUpdateEnabled(); metadata["user_color"] = m_operations->iconColor(); return metadata; } void DatabaseItem::setMetadata(const QString& key, QVariant value) { bool isResetValue = (value.isNull() || !value.canConvert() || value.toString().isEmpty()); if (key == "filter") { if (!m_filter.isEmpty() && isResetValue) return resetFilter(); else if (isResetValue) return; auto applyFilter = [this, value]() { QRegExp pattern(value.toString(), Qt::CaseSensitive, QRegExp::PatternSyntax::WildcardUnix); filterKeys(pattern); }; QByteArray val = value.toByteArray(); if (val.contains('*')) { return applyFilter(); } auto selfWPtr = getSelf(); auto openKeyCallback = QSharedPointer( new Operations::OpenKeyIfExistsCallback( selfWPtr, [applyFilter](const QString&, bool result) { if (!result) { applyFilter(); } })); auto self = selfWPtr.toStrongRef(); if (!self) return; m_operations->openKeyIfExists(val, self.dynamicCast(), openKeyCallback); return; } else if (key == "live_update") { if (liveUpdateTimer()->isActive() && isResetValue) { qDebug() << "Stop live update"; liveUpdateTimer()->stop(); } else { qDebug() << "Start live update"; liveUpdateTimer()->start(); } emit m_model.itemChanged(getSelf()); } } void DatabaseItem::getMemoryUsage(std::function callback) { if (m_childItems.size() == 0) { auto d = QSharedPointer>( new AsyncFuture::Deferred()); loadKeys([this, callback]() { lock(); AbstractNamespaceItem::getMemoryUsage(callback); }); } else { AbstractNamespaceItem::getMemoryUsage(callback); } } void DatabaseItem::unload(bool notify) { if (m_childItems.size() == 0) return; lock(); clear(); m_keysCount = 0; if (notify) m_operations->notifyDbWasUnloaded(m_dbIndex); unlock(); } void DatabaseItem::reload(std::function callback) { clear(); loadKeys([this, callback]() { QSettings settings; m_model.expandedNamespaces.clear(); if (settings.value("app/reopenNamespacesOnReload", true).toBool()) { auto self = getSelf().toStrongRef(); if (!self) return; restoreOpenedNamespaces(self.staticCast()); } if (callback) callback(); }); } void DatabaseItem::performLiveUpdate() { qDebug() << "Live update loading keys..."; if (isLocked()) { qDebug() << "Another loading operation is in progress. Skip this live update..."; liveUpdateTimer()->start(); return; } m_rawChildKeys.clear(); loadKeys( [this]() { QSettings settings; if (m_childItems.size() >= settings.value("app/liveUpdateKeysLimit", 1000).toInt()) { liveUpdateTimer()->stop(); emit m_model.itemChanged(getSelf()); QMessageBox::warning( nullptr, QCoreApplication::translate("RESP", "Live update was disabled"), QCoreApplication::translate( "RESP", "Live update was disabled due to exceeded keys limit. " "Please specify filter more carefully or change limit in " "settings.")); } else { liveUpdateTimer()->start(); emit m_model.itemChanged(getSelf()); } }, true); } void DatabaseItem::filterKeys(const QRegExp& filter) { m_filter = filter; emit m_model.itemChanged(getSelf()); reload(); } void DatabaseItem::resetFilter() { m_filter = QRegExp(m_operations->defaultFilter()); emit m_model.itemChanged(getSelf()); reload(); } QHash > DatabaseItem::eventHandlers() { auto events = AbstractNamespaceItem::eventHandlers(); events.insert("click", [this]() { if (m_childItems.size() != 0) { if (!isExpanded()) { setExpanded(true); m_model.expandItem(getSelf()); } return true; } loadKeys(); return false; }); events.insert("right-click", [this]() { if (m_childItems.size() != 0) return true; emit m_model.itemChanged(getSelf()); return true; }); events.insert("add_key", [this]() { auto callback = QSharedPointer( new Operations::OpenNewKeyDialogCallback(getSelf(), [this]() { confirmAction( nullptr, QCoreApplication::translate( "RESP", "Key was added. Do you want to reload keys in " "selected database?"), [this]() { reload(); m_keysCount++; }, QCoreApplication::translate("RESP", "Key was added")); })); m_operations->openNewKeyDialog(m_dbIndex, callback); return true; }); events.insert("reload", [this]() { reload(); return false; }); events.insert("flush", [this]() { confirmAction( nullptr, QCoreApplication::translate( "RESP", "Do you really want to remove all keys from this database?"), [this]() { auto callback = QSharedPointer( new Operations::FlushDbCallback( getSelf(), [this](const QString&) { unload(); })); m_operations->flushDb(m_dbIndex, callback); }); return true; }); events.insert("console", [this]() { m_operations->openConsoleTab(m_dbIndex); return true; }); events.insert("delete_keys", [this]() { m_operations->deleteDbKeys(*this); return true; }); events.insert("copy_keys", [this]() { m_operations->copyKeys(*this); return true; }); events.insert("rdb_import", [this]() { m_operations->importKeysFromRdb(*this); return true; }); events.insert("ttl", [this]() { m_operations->setTTL(*this); return true; }); return events; } QSharedPointer DatabaseItem::liveUpdateTimer() { if (!m_liveUpdateTimer) { QSettings settings; m_liveUpdateTimer = QSharedPointer(new QTimer()); m_liveUpdateTimer->setInterval( settings.value("app/liveUpdateInterval", 10).toInt() * 1000); qDebug() << "Live update timer" << settings.value("app/liveUpdateInterval", 10).toInt() * 1000; m_liveUpdateTimer->setSingleShot(true); QObject::connect(m_liveUpdateTimer.data(), &QTimer::timeout, [this]() { performLiveUpdate(); }); } return m_liveUpdateTimer; } bool DatabaseItem::isLiveUpdateEnabled() const { return m_liveUpdateTimer && m_liveUpdateTimer->isActive(); } // Top 10 filters QVariantList DatabaseItem::filterHistoryTop10() const { typedef QPair FilterUsage; QList filterHistoryRating; QVariantList filterHistoryList; auto server = parent().toStrongRef(); if (!server || !server.staticCast()) return filterHistoryList; QVariantMap filterHistory = m_operations->getFilterHistory(); QVariantMap::const_iterator i(filterHistory.begin()); while (i != filterHistory.end()) { FilterUsage filterUsage; filterUsage.first = i.key(); filterUsage.second = i.value().toInt(); filterHistoryRating.append(filterUsage); ++i; } std::sort(filterHistoryRating.begin(), filterHistoryRating.end(), [](FilterUsage i, FilterUsage j) { return (i.second > j.second); }); for (int i = 0; filterHistoryRating.size() > 0; i++) { if (i >= 10) break; filterHistoryList.append(filterHistoryRating.takeFirst().first); } return filterHistoryList; } ================================================ FILE: src/modules/connections-tree/items/databaseitem.h ================================================ #pragma once #include "abstractnamespaceitem.h" namespace ConnectionsTree { class ServerItem; class DatabaseItem : public AbstractNamespaceItem { public: DatabaseItem(unsigned int index, int keysCount, QSharedPointer operations, QWeakPointer parent, Model& model); ~DatabaseItem(); QByteArray getName() const override; QByteArray getFullPath() const override; QString getDisplayName() const override; QString type() const override { return "database"; } bool isEnabled() const override; QVariantMap metadata() const override; void setMetadata(const QString&, QVariant) override; void getMemoryUsage(std::function callback) override; void reload(std::function callback = std::function()); protected: void loadKeys(std::function callback = std::function(), bool partialReload=false); void unload(bool notify = true); void performLiveUpdate(); void filterKeys(const QRegExp& filter); void resetFilter(); QHash> eventHandlers() override; private: QSharedPointer liveUpdateTimer(); bool isLiveUpdateEnabled() const; QVariantList filterHistoryTop10() const; private: unsigned int m_keysCount; QSharedPointer m_liveUpdateTimer; }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/items/keyitem.cpp ================================================ #include "keyitem.h" #include #include #include #include #include #include "app/apputils.h" #include "connections-tree/items/abstractnamespaceitem.h" #include "connections-tree/model.h" #include "connections-tree/utils.h" using namespace ConnectionsTree; QSharedPointer parentTreeItemToNs( QWeakPointer p) { auto parentNs = p.toStrongRef(); if (!parentNs || !parentNs.staticCast()) return QSharedPointer(); return parentNs.staticCast(); } KeyItem::KeyItem(const QByteArray& fullPath, QWeakPointer parent, Model& model, bool shortNameRendering) : TreeItem(model), m_fullPath(fullPath), m_parent(parent), m_removed(false), m_shortRendering(shortNameRendering) { } QString KeyItem::getDisplayName() const { QString title; if (m_parent && m_parent.toStrongRef()->type() == "namespace" && m_shortRendering) { auto parent = parentTreeItemToNs(m_parent); title = printableString(getFullPath().mid( parent->getFullPath().size() + parent->operations()->getNamespaceSeparator().size())); } else { title = printableString(getFullPath(), true); } if (m_usedMemory > 0) { title.append(QString(" [%1]").arg(humanReadableSize(m_usedMemory))); } return title; } QByteArray KeyItem::getName() const { return getFullPath(); } QList> KeyItem::getAllChilds() const { return QList>(); } bool KeyItem::supportChildItems() const { return false; } uint KeyItem::childCount(bool) const { return 0u; } QSharedPointer KeyItem::child(uint) { return QSharedPointer(); } QWeakPointer KeyItem::parent() const { return m_parent; } bool KeyItem::isEnabled() const { if (!m_removed && m_parent) { return m_parent.toStrongRef()->isEnabled(); } else { return m_removed == false; } } QByteArray KeyItem::getFullPath() const { return m_fullPath; } int KeyItem::getDbIndex() const { auto parentNs = parentTreeItemToNs(m_parent); if (!parentNs) { return -1; } return parentNs->getDbIndex(); } void KeyItem::setRemoved() { m_removed = true; emit m_model.itemChanged(getSelf()); } void KeyItem::getMemoryUsage(std::function callback) { auto parentNs = parentTreeItemToNs(m_parent); if (!parentNs || !parentNs->operations()) return callback(0); auto cb = QSharedPointer( new Operations::GetUsedMemoryCallback( getSelf(), [this, callback](qlonglong result) { m_usedMemory = result; callback(result); emit m_model.itemChanged(getSelf()); })); parentNs->operations()->getUsedMemory( {getFullPath()}, getDbIndex(), cb, QSharedPointer()); } void KeyItem::setFullPath(const QByteArray& p) { m_fullPath = p; emit m_model.itemChanged(getSelf()); } QHash> KeyItem::eventHandlers() { auto events = TreeItem::eventHandlers(); events.insert("click", [this]() { if (!isEnabled()) return true; auto parentNs = parentTreeItemToNs(m_parent); if (!parentNs || !parentNs->operations()) return true; parentNs->operations()->openKeyTab( getSelf().toStrongRef().staticCast(), false); return true; }); events.insert("mid-click", [this]() { if (!isEnabled()) return true; auto parentNs = parentTreeItemToNs(m_parent); if (!parentNs || !parentNs->operations()) return true; parentNs->operations()->openKeyTab( getSelf().toStrongRef().staticCast(), true); return true; }); events.insert("delete", [this]() { confirmAction( nullptr, QCoreApplication::translate("RESP", "Do you really want to delete this key?"), [this]() { auto parentNs = parentTreeItemToNs(m_parent); if (!parentNs || !parentNs->operations()) return; auto callback = QSharedPointer( new Operations::DeleteDbKeyCallback( getSelf(), [this](const QString& err) { emit m_model.error(QCoreApplication::translate( "RESP", "Cannot delete key:\n\n") + err); return; })); parentNs->operations()->deleteDbKey(*this, callback); }); return true; }); return events; } ================================================ FILE: src/modules/connections-tree/items/keyitem.h ================================================ #pragma once #include "connections-tree/operations.h" #include "memoryusage.h" #include "treeitem.h" namespace ConnectionsTree { class KeyItem : public TreeItem, public MemoryUsage { public: KeyItem(const QByteArray& fullPath, QWeakPointer parent, Model& model, bool shortNameRendering); QString getDisplayName() const override; QByteArray getName() const override; QString type() const override { return "key"; } QList> getAllChilds() const override; bool supportChildItems() const override; uint childCount(bool recursive = false) const override; QSharedPointer child(uint) override; QWeakPointer parent() const override; bool isEnabled() const override; QByteArray getFullPath() const override; int getDbIndex() const; void setRemoved(); void setFullPath(const QByteArray& p); void getMemoryUsage(std::function callback) override; protected: QHash> eventHandlers() override; private: QByteArray m_fullPath; QWeakPointer m_parent; bool m_removed; bool m_shortRendering; }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/items/loadmoreitem.cpp ================================================ #include "loadmoreitem.h" ConnectionsTree::LoadMoreItem::LoadMoreItem(QWeakPointer parent, Model &model) : ConnectionsTree::TreeItem::TreeItem(model), m_parent(parent) { } QString ConnectionsTree::LoadMoreItem::getDisplayName() const { return QCoreApplication::translate("RESP", "Load more keys"); } QList > ConnectionsTree::LoadMoreItem::getAllChilds() const { return {}; } bool ConnectionsTree::LoadMoreItem::supportChildItems() const { return false; } uint ConnectionsTree::LoadMoreItem::childCount(bool) const { return 0u; } QSharedPointer ConnectionsTree::LoadMoreItem::child(uint) { return QSharedPointer(); } QWeakPointer ConnectionsTree::LoadMoreItem::parent() const { return m_parent; } bool ConnectionsTree::LoadMoreItem::isEnabled() const { return true; } QHash > ConnectionsTree::LoadMoreItem::eventHandlers() { QHash> events; events["click"] = [this]() { auto parentPtr = m_parent.toStrongRef(); if (!parentPtr) { return true; } parentPtr->fetchMore(); return false; }; return events; } ================================================ FILE: src/modules/connections-tree/items/loadmoreitem.h ================================================ #pragma once #include "connections-tree/operations.h" #include "treeitem.h" namespace ConnectionsTree { class LoadMoreItem : public TreeItem { public: LoadMoreItem(QWeakPointer parent, Model& model); QString getDisplayName() const override; QString type() const override { return "loader"; } QList> getAllChilds() const override; bool supportChildItems() const override; uint childCount(bool recursive = false) const override; QSharedPointer child(uint) override; QWeakPointer parent() const override; bool isEnabled() const override; protected: QHash> eventHandlers() override; private: QWeakPointer m_parent; }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/items/memoryusage.h ================================================ #pragma once #include #include #include #include #include "treeitem.h" namespace ConnectionsTree { class MemoryUsage { public: MemoryUsage() : m_usedMemory(0) {} virtual ~MemoryUsage() {} virtual void getMemoryUsage(std::function callback) = 0; qlonglong usedMemory() const { return m_usedMemory; } protected: qlonglong m_usedMemory; QMutex m_updateUsedMemoryMutex; }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/items/namespaceitem.cpp ================================================ #include "namespaceitem.h" #include #include #include #include "app/apputils.h" #include "connections-tree/model.h" #include "connections-tree/utils.h" #include "connections-tree/keysrendering.h" #include "databaseitem.h" #include "keyitem.h" #include "loadmoreitem.h" using namespace ConnectionsTree; NamespaceItem::NamespaceItem(const QByteArray &fullPath, QSharedPointer operations, QWeakPointer parent, Model &model, uint dbIndex, QRegExp filter) : AbstractNamespaceItem(model, parent, operations, dbIndex, filter), m_fullPath(fullPath), m_removed(false) {} QString NamespaceItem::getDisplayName() const { QString title = QString("%1 (%2)") .arg(printableString(getName(), true)) .arg(keysCount()); if (m_usedMemory > 0) { title.append(QString(" [%1]").arg(humanReadableSize(m_usedMemory))); } return title; } QByteArray NamespaceItem::getName() const { qsizetype pos = m_fullPath.lastIndexOf(m_operations->getNamespaceSeparator()); if (pos >= 0) { return m_fullPath.mid(pos + m_operations->getNamespaceSeparator().size()); } else { return m_fullPath; } } bool NamespaceItem::isEnabled() const { return m_removed == false; } QByteArray NamespaceItem::getFullPath() const { return m_fullPath; } void NamespaceItem::setRemoved() { m_removed = true; clear(); emit m_model.itemChanged(getSelf()); } QVariantMap NamespaceItem::metadata() const { QVariantMap metadata = TreeItem::metadata(); metadata["full_path"] = getFullPath(); return metadata; } void NamespaceItem::load() { auto onKeysRendered = QSharedPointer( new RenderRawKeysCallback(getSelf(), [this]() { ensureLoaderIsCreated(); unlock(); setExpanded(true); emit m_model.itemChanged(getSelf()); m_model.expandItem(getSelf()); })); if (m_rawChildKeys.size() > 0) { auto rawKeys = m_rawChildKeys; m_rawChildKeys.clear(); return renderRawKeys(rawKeys, m_filter, onKeysRendered, true, false); } QString nsFilter = QString("%1%2*") .arg(QString::fromUtf8(m_fullPath)) .arg(m_operations->getNamespaceSeparator()); if (!m_filter.isEmpty()) { if (m_filter.pattern().startsWith(nsFilter.chopped(1))) { nsFilter = m_filter.pattern(); } else { nsFilter = QString("%1%2%3") .arg(QString::fromUtf8(m_fullPath)) .arg(m_operations->getNamespaceSeparator()) .arg(m_filter.pattern()); } } auto callback = QSharedPointer( new Operations::LoadNamespaceItemsCallback( getSelf(), [this, nsFilter, onKeysRendered]( const RedisClient::Connection::RawKeysList &keylist, const QString &err) { if (!err.isEmpty()) { unlock(); return showLoadingError(err); } return renderRawKeys(keylist, m_filter, onKeysRendered, true, false); })); m_operations->loadNamespaceItems(m_dbIndex, nsFilter, callback); } void NamespaceItem::reload() { clear(); load(); } QHash> NamespaceItem::eventHandlers() { auto events = AbstractNamespaceItem::eventHandlers(); events.insert("click", [this]() { if (m_childItems.size() == 0) { load(); return false; } else if (!isExpanded()) { setExpanded(true); emit m_model.itemChanged(getSelf()); m_model.expandItem(getSelf()); } return true; }); events.insert("add_key", [this]() { auto callback = QSharedPointer( new Operations::OpenNewKeyDialogCallback(getSelf(), [this]() { confirmAction( nullptr, QCoreApplication::translate( "RESP", "Key was added. Do you want to reload keys in " "selected namespace?"), [this]() { reload(); }, QCoreApplication::translate("RESP", "Key was added")); })); m_operations->openNewKeyDialog( m_dbIndex, callback, QString("%1%2") .arg(QString::fromUtf8(getFullPath())) .arg(m_operations->getNamespaceSeparator())); return true; }); events.insert("reload", [this]() { reload(); return false; }); events.insert("delete", [this]() { m_operations->deleteDbNamespace(*this); return true; }); return events; } ================================================ FILE: src/modules/connections-tree/items/namespaceitem.h ================================================ #pragma once #include "abstractnamespaceitem.h" namespace ConnectionsTree { class NamespaceItem : public AbstractNamespaceItem { Q_OBJECT public: NamespaceItem(const QByteArray& fullPath, QSharedPointer operations, QWeakPointer parent, Model& model, uint dbIndex); public: NamespaceItem(const QByteArray& fullPath, QSharedPointer operations, QWeakPointer parent, Model& model, uint dbIndex, QRegExp filter); QString getDisplayName() const override; QByteArray getName() const override; QByteArray getFullPath() const override; QString type() const override { return "namespace"; } bool isEnabled() const override; void setRemoved(); QVariantMap metadata() const override; protected: void load(); void reload(); QHash> eventHandlers() override; private: QByteArray m_fullPath; bool m_removed; }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/items/servergroup.cpp ================================================ #include "servergroup.h" #include "connections-tree/model.h" #include "connections-tree/items/serveritem.h" #include "connections-tree/utils.h" using namespace ConnectionsTree; ServerGroup::ServerGroup(const QString& name, Model& model) : SortableTreeItem(model), m_name(name) {} QString ServerGroup::getDisplayName() const { return m_name; } QList > ServerGroup::getAllChilds() const { return m_servers; } uint ServerGroup::childCount(bool) const { return static_cast(m_servers.size()); } QSharedPointer ServerGroup::child(uint row) { if (row < m_servers.size()) { return m_servers.at(row); } return QSharedPointer(); } QSharedPointer ServerGroup::takeChild(uint row) { if (!child(row)) return QSharedPointer(); return m_servers.takeAt(row); } void ServerGroup::insertChildAt(uint row, QSharedPointer srv) { if (!srv) return; m_servers.insert(row, srv); } void ServerGroup::removeConnection(QSharedPointer srv) { m_servers.removeAll(srv); } QHash > ServerGroup::eventHandlers() { auto events = TreeItem::eventHandlers(); events.insert("edit", [this]() { emit editActionRequested(); return true; }); events.insert("delete", [this]() { confirmAction(nullptr, QCoreApplication::translate( "RESP", "Do you really want to delete group with all connections?"), [this]() { emit deleteActionRequested(); }); return true; }); return events; } void ServerGroup::setName(const QString& name) { m_name = name; } void ServerGroup::addServer(QSharedPointer s) { auto srv = s.dynamicCast(); m_servers.append(s); } bool ServerGroup::isExpanded() const { return true; } void ServerGroup::unload() { for (auto child : m_servers) { auto item = child.dynamicCast(); if (!item) continue; item->unload(); } } ================================================ FILE: src/modules/connections-tree/items/servergroup.h ================================================ #pragma once #include #include #include "connections-tree/operations.h" #include "sortabletreeitem.h" namespace ConnectionsTree { class Model; class ServerGroup : public QObject, public SortableTreeItem { Q_OBJECT public: ServerGroup(const QString &name, Model& m); QString getDisplayName() const override; QString type() const override { return "server_group"; } QList> getAllChilds() const override; uint childCount(bool recursive = false) const override; QSharedPointer child(uint row) override; QSharedPointer takeChild(uint row); void insertChildAt(uint row, QSharedPointer srv); void removeConnection(QSharedPointer srv); void setName(const QString &name); void addServer(QSharedPointer s); bool isExpanded() const override; void unload() override; signals: void editActionRequested(); void deleteActionRequested(); protected: QHash> eventHandlers() override; private: QString m_name; QList> m_servers; }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/items/serveritem.cpp ================================================ #include "serveritem.h" #include #include #include #include #include #include #include #include "connections-tree/model.h" #include "connections-tree/utils.h" #include "databaseitem.h" using namespace ConnectionsTree; ServerItem::ServerItem(QSharedPointer operations, Model& model, QWeakPointer parent) : SortableTreeItem(model), m_operations(operations), m_parent(parent) {} ServerItem::~ServerItem() {} QString ServerItem::getDisplayName() const { return m_operations->connectionName(); } QList > ServerItem::getAllChilds() const { return m_databases; } uint ServerItem::childCount(bool) const { return static_cast(m_databases.size()); } QSharedPointer ServerItem::child(uint row) { if (row < m_databases.size()) { return m_databases.at(row); } return QSharedPointer(); } QWeakPointer ServerItem::parent() const { return m_parent; } void ServerItem::setParent(QWeakPointer p) { m_parent = p; } bool ServerItem::isDatabaseListLoaded() const { return m_databases.size() > 0; } QSharedPointer ServerItem::getOperations() { return m_operations; } int ServerItem::row() const { if (!parent()) { return m_row; } return TreeItem::row(); } void ServerItem::load() { auto callback = QSharedPointer( new Operations::GetDatabasesCallback( getSelf(), [this](RedisClient::DatabaseList databases, const QString& err) { if (err.size() > 0) { unlock(); emit m_model.error(QCoreApplication::translate( "RESP", "Cannot load databases:\n\n") + err); return; } if (databases.size() == 0) { unlock(); return; } RedisClient::DatabaseList::const_iterator db = databases.constBegin(); QList> dbs; while (db != databases.constEnd()) { QSharedPointer database((new DatabaseItem( db.key(), db.value(), m_operations, m_self, m_model))); dbs.push_back(database); ++db; } QTimer::singleShot(0, this, [this, dbs]() { m_model.beforeChildLoaded(getSelf(), dbs.size()); m_databases = dbs; m_model.childLoaded(getSelf()); unlock(); m_model.expandItem(getSelf()); }); })); m_currentOperation = m_operations->getDatabases(callback); if (!m_currentOperation.isRunning()) { unlock(); } } void ServerItem::unload() { if (!isDatabaseListLoaded()) return; m_model.beforeItemChildsUnloaded(m_self); m_operations->disconnect(); for (auto db : m_databases) { auto dbItem = db.staticCast(); if (dbItem && m_operations) { m_operations->notifyDbWasUnloaded(dbItem->getDbIndex()); } } m_databases.clear(); m_model.itemChildRemoved(getSelf()); } void ServerItem::reload() { unload(); load(); } void ServerItem::edit() { unload(); emit editActionRequested(); } void ServerItem::remove() { unload(); emit deleteActionRequested(); } void ServerItem::openConsole() { m_operations->openConsoleTab(); } QHash > ServerItem::eventHandlers() { auto events = TreeItem::eventHandlers(); events.insert("click", [this]() { m_operations->openServerStats(); if (isDatabaseListLoaded()) { if (!isExpanded()) { setExpanded(true); m_model.expandItem(getSelf()); } return true; } load(); return false; }); events.insert("console", [this]() { m_operations->openConsoleTab(); return true; }); auto openServerContextTab = [this]() { m_operations->openServerStats(); return true; }; events.insert("right-click", openServerContextTab); events.insert("mid-click", openServerContextTab); events.insert("duplicate", [this]() { m_operations->duplicateConnection(); return true; }); events.insert("reload", [this]() { reload(); return false; }); events.insert("unload", [this]() { unload(); return true; }); events.insert("edit", [this]() { auto unloadAction = [this]() { unload(); emit editActionRequested(); }; if (m_operations->isConnected()) { confirmAction(nullptr, QCoreApplication::translate( "RESP", "Value and Console tabs related to this " "connection will be closed. Do you want to continue?"), unloadAction); } else { unloadAction(); } return true; }); events.insert("delete", [this]() { confirmAction(nullptr, QCoreApplication::translate( "RESP", "Do you really want to delete connection?"), [this]() { unload(); emit deleteActionRequested(); }); return true; }); return events; } void ServerItem::setWeakPointer(QWeakPointer self) { m_self = self; m_selfPtr = self; } QVariantMap ConnectionsTree::ServerItem::metadata() const { QVariantMap meta = TreeItem::metadata(); if (isDatabaseListLoaded()) { meta["server_type"] = m_operations->mode(); } else { meta["server_type"] = "unknown"; } meta["user_color"] = m_operations->iconColor(); return meta; } ================================================ FILE: src/modules/connections-tree/items/serveritem.h ================================================ #pragma once #include #include #include "connections-tree/operations.h" #include "sortabletreeitem.h" namespace ConnectionsTree { class Model; class ServerItem : public QObject, public SortableTreeItem { Q_OBJECT public: ServerItem(QSharedPointer operations, Model &model, QWeakPointer parent = QWeakPointer()); ~ServerItem(); QString getDisplayName() const override; QString type() const override { return "server"; } QVariantMap metadata() const override; QList> getAllChilds() const override; uint childCount(bool recursive = false) const override; QSharedPointer child(uint row) override; QWeakPointer parent() const override; void setParent(QWeakPointer p); void setWeakPointer(QWeakPointer); bool isDatabaseListLoaded() const; QSharedPointer getOperations(); int row() const override; public slots: void unload() override; private slots: void load(); void reload(); void edit(); void remove(); void openConsole(); signals: void editActionRequested(); void deleteActionRequested(); protected: QHash> eventHandlers() override; private: QSharedPointer m_operations; QList> m_databases; QWeakPointer m_self; QWeakPointer m_parent; QModelIndex m_index; }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/items/sortabletreeitem.h ================================================ #pragma once #include "treeitem.h" namespace ConnectionsTree { class Model; class SortableTreeItem : public TreeItem { public: SortableTreeItem(Model& model) : TreeItem(model), m_row(0) {} int row() const override { return m_row; } void setRow(int r) { m_row = r; } virtual void unload() = 0; protected: int m_row; }; } ================================================ FILE: src/modules/connections-tree/items/treeitem.cpp ================================================ #include "treeitem.h" #include "connections-tree/model.h" ConnectionsTree::TreeItem::TreeItem(Model &m) : m_model(m), m_locked(false), m_expanded(false) {} QVariantMap ConnectionsTree::TreeItem::metadata() const { QVariantMap meta; meta["name"] = getDisplayName(); meta["full_name"] = getName(); meta["type"] = type(); meta["locked"] = isLocked(); meta["state"] = isEnabled(); return meta; } int ConnectionsTree::TreeItem::row() const { if (!parent()) return 0; auto p = parent().toStrongRef(); for (uint index = 0; index < p->childCount(); ++index) { if (p->child(index).data() == this) return index; } return 0; } QWeakPointer ConnectionsTree::TreeItem::getSelf() { if (m_selfPtr) return m_selfPtr; if (!parent()) return QWeakPointer(); QSharedPointer p = parent().toStrongRef(); if (!p) return QWeakPointer(); m_selfPtr = p->child(row()).toWeakRef(); return m_selfPtr; } ConnectionsTree::Model &ConnectionsTree::TreeItem::model() { return m_model; } void ConnectionsTree::TreeItem::lock() { m_locked = true; if (getSelf()) emit m_model.itemChanged(getSelf()); } void ConnectionsTree::TreeItem::unlock() { m_locked = false; if (getSelf()) emit m_model.itemChanged(getSelf()); } QHash> ConnectionsTree::TreeItem::eventHandlers() { QHash> events; events["cancel"] = [this]() { cancelCurrentOperation(); return true; }; return events; } void ConnectionsTree::TreeItem::handleEvent(QString event) { if (!eventHandlers().contains(event)) return; if (isLocked() && event != "cancel") { qDebug() << "Item is locked. Ignore event: " << event; emit m_model.itemChanged(getSelf()); return; } auto handler = eventHandlers()[event]; try { lock(); bool shouldUnlock = handler(); if (shouldUnlock) { unlock(); } } catch (...) { qWarning() << "Error on event processing: " << event; unlock(); } } void ConnectionsTree::TreeItem::cancelCurrentOperation() { m_currentOperation.cancel(); } ================================================ FILE: src/modules/connections-tree/items/treeitem.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include namespace ConnectionsTree { class Model; class TreeItem { public: TreeItem(Model& m); virtual ~TreeItem() {} virtual QString getDisplayName() const = 0; virtual QByteArray getName() const { return getDisplayName().toUtf8(); } virtual QByteArray getFullPath() const { return QByteArray(); } virtual QString type() const = 0; virtual QList> getAllChilds() const = 0; virtual uint childCount(bool recursive = false) const = 0; virtual QSharedPointer child(uint row) = 0; virtual void removeChild(int) {}; virtual QWeakPointer parent() const { return QWeakPointer(); } virtual bool supportChildItems() const { return true; } virtual QVariantMap metadata() const; virtual void setMetadata(const QString&, QVariant) {} virtual int row() const; virtual QWeakPointer getSelf(); virtual void handleEvent(QString event); virtual void cancelCurrentOperation(); virtual bool isLocked() const { return m_locked; } virtual bool isEnabled() const { return true; }; virtual bool isExpanded() const { return m_expanded; } virtual void setExpanded(bool v) { m_expanded = v; } virtual void fetchMore() {} virtual Model& model(); protected: void lock(); void unlock(); virtual QHash > eventHandlers(); protected: Model& m_model; QWeakPointer m_selfPtr; bool m_locked; bool m_expanded; QFuture m_currentOperation; }; typedef QList> TreeItems; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/keysrendering.cpp ================================================ #include "keysrendering.h" #include #include "items/abstractnamespaceitem.h" #include "items/keyitem.h" #include "items/namespaceitem.h" #include "model.h" using namespace ConnectionsTree; QSharedPointer ConnectionsTree::resolveRootItem(QSharedPointer item) { if (!item) return QSharedPointer(); if (item->type() == "database") { return item; } auto parent = item->parent().toStrongRef(); if (!parent) return QSharedPointer(); if (parent->type() == "database") return parent.dynamicCast(); return resolveRootItem(parent.dynamicCast()); } void KeysTreeRenderer::renderKeys(QSharedPointer operations, RedisClient::Connection::RawKeysList keys, QSharedPointer parent, RenderingSettigns settings, const QSet &expandedNamespaces) { // init QElapsedTimer timer; timer.start(); int unprocessedPartStart = 0; if (parent->getFullPath().size() > 0 || parent->type() == "namespace") { unprocessedPartStart = parent->getFullPath().size() + settings.nsSeparator.length(); } auto rootItem = resolveRootItem(parent); QHash> preRenderedKeys; if (rootItem) { qDebug() << "Root item resolved"; preRenderedKeys = rootItem->getKeysIndex(); } qDebug() << "Pre-rendered keys: " << preRenderedKeys.size(); auto preRenderedKeysList = preRenderedKeys.keys(); QSet preRenderedKeysSet = QSet(preRenderedKeysList.begin(), preRenderedKeysList.end()); QSet preRenderedKeysToBeRemoved; if (settings.checkPreRenderedItems) { preRenderedKeysToBeRemoved = QSet(preRenderedKeysSet.begin(), preRenderedKeysSet.end()); } qDebug() << "Live update: " << settings.checkPreRenderedItems; QByteArray rawKey; QByteArray nextKey; QList bulkInsertItems; auto isBulkInsert = [settings, preRenderedKeysSet, unprocessedPartStart]( const QByteArray ¤t, const QByteArray &next) { return (settings.appendNewItems && current.indexOf(settings.nsSeparator, unprocessedPartStart) == -1 && !next.isEmpty() && next.indexOf(settings.nsSeparator, unprocessedPartStart) == -1 && !preRenderedKeysSet.contains(next)); }; while (!keys.isEmpty()) { rawKey = keys.takeFirst(); if (preRenderedKeysSet.contains(rawKey)) { if (preRenderedKeysToBeRemoved.contains(rawKey)) { preRenderedKeysToBeRemoved.remove(rawKey); } continue; } if (keys.size() > 0) { nextKey = keys[0]; } else { nextKey = QByteArray(); } if (isBulkInsert(rawKey, nextKey)) { bulkInsertItems.append(rawKey); continue; } else if (bulkInsertItems.size() > 0 && parent) { int itemsAboutToBeInserted = qMin(static_cast(bulkInsertItems.size()), settings.renderLimit - parent->getAllChilds().size()); qDebug() << "Bulk insert" << itemsAboutToBeInserted; if (itemsAboutToBeInserted > 0) parent->model().beforeChildLoaded(parent.toWeakRef(), itemsAboutToBeInserted); for (const auto &item : bulkInsertItems) { if (parent->getAllChilds().size() >= settings.renderLimit) { parent->appendRawKey(item); } else { QSharedPointer newKey(new KeyItem( item, parent, parent->model(), settings.shortKeysRendering)); parent->append(newKey, false); if (rootItem && rootItem->type() == "database") { rootItem->appendKeyToIndex(newKey); } } } if (itemsAboutToBeInserted > 0) parent->model().childLoaded(parent.toWeakRef()); bulkInsertItems.clear(); } try { renderLazily(rootItem, parent, rawKey.mid(unprocessedPartStart), rawKey, operations, settings, expandedNamespaces, 0, nextKey); } catch (std::bad_alloc &) { parent->showLoadingError("Not enough memory to render all keys"); break; } } if (preRenderedKeysToBeRemoved.size() > 0) { QList> obsoleteKeys; for (const auto &keyFullPath : qAsConst(preRenderedKeysToBeRemoved)) { obsoleteKeys.append(preRenderedKeys[keyFullPath]); } parent->removeObsoleteKeys(obsoleteKeys); } qDebug() << "Tree builded in: " << timer.elapsed() << " ms"; } void KeysTreeRenderer::renderLazily(QSharedPointer root, QSharedPointer parent, const QByteArray ¬ProcessedKeyPart, const QByteArray &fullKey, QSharedPointer m_operations, const RenderingSettigns &settings, const QSet &expandedNamespaces, unsigned long level, const QByteArray &nextKey) { Q_ASSERT(parent); if (level > 0 && parent->isExpanded() == false) { parent->appendRawKey(fullKey); return; } QWeakPointer currentParent = parent.staticCast().toWeakRef(); int indexOfNaspaceSeparator = (settings.nsSeparator.isEmpty()) ? -1 : notProcessedKeyPart.indexOf(settings.nsSeparator); if (indexOfNaspaceSeparator == -1) { if (parent->getAllChilds().size() >= settings.renderLimit) { parent->appendRawKey(fullKey); } else { QSharedPointer newKey(new KeyItem(fullKey, currentParent, parent->model(), settings.shortKeysRendering)); if (settings.appendNewItems) { parent->append(newKey); } else { parent->insertChild(newKey); } if (root && root->type() == "database") { root->appendKeyToIndex(newKey); } } return; } QByteArray firstNamespaceName = notProcessedKeyPart.mid(0, indexOfNaspaceSeparator); QSharedPointer namespaceItem = parent->findChildNamespace(firstNamespaceName); if (namespaceItem.isNull()) { long nsPos = fullKey.size() - notProcessedKeyPart.size() + firstNamespaceName.size(); QByteArray namespaceFullPath = fullKey.mid(0, nsPos); // Single namespaced key if (nextKey.isEmpty() || nextKey.indexOf(namespaceFullPath) == -1) { QSharedPointer newKey(new KeyItem(fullKey, currentParent, parent->model(), settings.shortKeysRendering)); parent->append(newKey); if (root && root->type() == "database") { root->appendKeyToIndex(newKey); } return; } namespaceItem = QSharedPointer( new NamespaceItem(namespaceFullPath, m_operations, currentParent, parent->model(), settings.dbIndex, settings.filter)); if (expandedNamespaces.contains(namespaceFullPath)) { namespaceItem->setExpanded(true); } parent->appendNamespace(namespaceItem); } renderLazily(root, namespaceItem, notProcessedKeyPart.mid(indexOfNaspaceSeparator + settings.nsSeparator.length()), fullKey, m_operations, settings, expandedNamespaces, level + 1, nextKey); } ================================================ FILE: src/modules/connections-tree/keysrendering.h ================================================ #pragma once #include #include #include #include #include namespace ConnectionsTree { class Operations; class AbstractNamespaceItem; class Model; QSharedPointer resolveRootItem(QSharedPointer item); class KeysTreeRenderer { public: struct RenderingSettigns { QRegExp filter; QString nsSeparator; uint dbIndex; uint renderLimit; bool appendNewItems; bool checkPreRenderedItems; bool shortKeysRendering; }; public: static void renderKeys(QSharedPointer operations, RedisClient::Connection::RawKeysList keys, QSharedPointer parent, RenderingSettigns settings, const QSet &expandedNamespaces); private: static void renderLazily(QSharedPointer root, QSharedPointer parent, const QByteArray ¬ProcessedKeyPart, const QByteArray &fullKey, QSharedPointer operations, const RenderingSettigns& settings, const QSet &expandedNamespaces, unsigned long level=0, const QByteArray &nextKey=QByteArray()); }; } ================================================ FILE: src/modules/connections-tree/model.cpp ================================================ #include "model.h" #include #include #include #include #include "items/serveritem.h" #include "items/servergroup.h" #include "items/databaseitem.h" using namespace ConnectionsTree; Model::Model(QObject *parent) : QAbstractItemModel(parent), m_rawPointers(new QHash>()) { qRegisterMetaType>("QWeakPointer"); QObject::connect(this, &Model::itemChanged, this, &Model::onItemChanged); } QVariant Model::data(const QModelIndex &index, int role) const { const TreeItem *item = getItemFromIndex(index); if (item == nullptr) return QVariant(); if (role == itemMetaData) return item->metadata(); return QVariant(); } QHash Model::roleNames() const { QHash roles; roles[itemMetaData] = "metadata"; return roles; } Qt::ItemFlags Model::flags(const QModelIndex &index) const { const TreeItem *item = getItemFromIndex(index); if (item == nullptr) return Qt::NoItemFlags; Qt::ItemFlags result = Qt::ItemIsSelectable; if (item->isEnabled()) result |= Qt::ItemIsEnabled; return result; } QModelIndex Model::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) return QModelIndex(); TreeItem *parentItem = getItemFromIndex(parent); QSharedPointer childItem; // get item from root items if (parentItem) { childItem = parentItem->child(row); } else if (row < m_treeItems.size()) { childItem = m_treeItems.at(row); } if (!childItem) return QModelIndex(); m_rawPointers->insert(childItem.data(), childItem.toWeakRef()); return createIndex(row, column, childItem.data()); } QModelIndex Model::parent(const QModelIndex &index) const { const TreeItem *childItem = getItemFromIndex(index); if (!childItem) return QModelIndex(); QWeakPointer parentItem = childItem->parent(); if (!parentItem) return QModelIndex(); auto parentStrongRef = parentItem.toStrongRef(); if (!parentStrongRef) return QModelIndex(); m_rawPointers->insert(parentStrongRef.data(), parentItem); return createIndex(parentStrongRef->row(), 0, parentStrongRef.data()); } int Model::rowCount(const QModelIndex &parent) const { const TreeItem *parentItem = getItemFromIndex(parent); if (!parentItem) return m_treeItems.size(); return parentItem->childCount(); } bool Model::hasChildren(const QModelIndex &parent) const { const TreeItem *parentItem = getItemFromIndex(parent); if (!parentItem) return m_treeItems.size() > 0; if (!parentItem->supportChildItems()) return false; return parentItem->childCount() > 0; } QModelIndex Model::getIndexFromItem(QWeakPointer item) { if (!item) { return QModelIndex(); } auto sRef = item.toStrongRef(); if (!sRef) { return QModelIndex(); } if (!sRef->parent()) { return index(sRef->row(), 0, QModelIndex()); } m_rawPointers->insert(sRef.data(), item); return createIndex(sRef->row(), 0, (void *)sRef.data()); } void Model::onItemChanged(QWeakPointer item) { if (!item) return; auto index = getIndexFromItem(item); if (!index.isValid()) return; emit dataChanged(index, index); } void Model::beforeItemChildsUnloaded(QWeakPointer item) { if (!item) return; auto index = getIndexFromItem(item); if (!index.isValid()) return; auto itemPtr = item.toStrongRef(); if (!itemPtr || itemPtr->childCount() == 0) return; beginRemoveRows(index, 0, itemPtr->childCount() - 1); } void Model::beforeChildLoadedAtPos(QWeakPointer item, int pos) { if (!item) return; auto index = getIndexFromItem(item); if (!index.isValid()) return; beginInsertRows(index, pos, pos); } void Model::beforeChildLoaded(QWeakPointer item, int count) { if (!item) return; auto index = getIndexFromItem(item); if (!index.isValid()) return; auto treeItem = item.toStrongRef(); if (!treeItem) return; beginInsertRows(index, treeItem->getAllChilds().size(), treeItem->getAllChilds().size() + count - 1); } void Model::childLoaded(QWeakPointer item) { if (!item) return; auto index = getIndexFromItem(item); if (!index.isValid()) return; endInsertRows(); } void Model::beforeItemChildRemoved(QWeakPointer item, int row) { if (!item) return; auto index = getIndexFromItem(item); if (!index.isValid()) return; beginRemoveRows(index, row, row); } void Model::itemChildRemoved(QWeakPointer childItem) { if (!childItem) return; endRemoveRows(); } void Model::expandItem(QWeakPointer item) { if (!item) return; auto index = getIndexFromItem(item); if (!index.isValid()) return; emit expand(index); } void Model::iterateAllChilds(QSharedPointer item, QList& pendingChanges) { if (!item->isExpanded()) { return; } for (long rowIndex = 0; rowIndex < item->childCount(); rowIndex++) { auto child = item->child(rowIndex); pendingChanges.append({child, getIndexFromItem(child)}); iterateAllChilds(child, pendingChanges); } } void Model::beforeItemLayoutChanged(QWeakPointer item) { if (!item) return; auto itemS = item.toStrongRef(); auto index = getIndexFromItem(item); if (!index.isValid()) return; if (m_pendingChanges.contains(item)) { m_pendingChanges.remove(item); } emit layoutAboutToBeChanged({}, QAbstractItemModel::VerticalSortHint); QList pendingChanges; iterateAllChilds(itemS, pendingChanges); m_pendingChanges[item] = pendingChanges; } void Model::itemLayoutChanged(QWeakPointer item) { if (!item) return; auto itemS = item.toStrongRef(); auto index = getIndexFromItem(item); if (!index.isValid()) return; if (!m_pendingChanges.contains(item)) { qWarning() << "Item " << item << " doesnt have pending layout changes"; return; } auto changeIndexes = m_pendingChanges.take(item); while (changeIndexes.size() > 0) { auto change = changeIndexes.takeFirst(); auto child = change.first.toStrongRef(); if (!child) { qDebug() << "Layout change: Child was removed. Skipping"; continue; } auto from = change.second; auto to = getIndexFromItem(child); changePersistentIndex(from, to); } emit layoutChanged({}, QAbstractItemModel::VerticalSortHint); for (long rowIndex = 0; rowIndex < itemS->childCount(); rowIndex++) { auto child = itemS->child(rowIndex); auto childIndex = getIndexFromItem(child); emit dataChanged(childIndex, childIndex); } } void Model::setMetadata(const QModelIndex &index, const QString &metaKey, QVariant value) { TreeItem *item = getItemFromIndex(index); if (item == nullptr) return; item->setMetadata(metaKey, value); } void Model::sendEvent(const QModelIndex &index, QString event) { TreeItem *item = getItemFromIndex(index); if (!item) return; item->handleEvent(event); } int Model::size() { return m_treeItems.size(); } void Model::setExpanded(const QModelIndex &index) { TreeItem *item = getItemFromIndex(index); if (!item) return; item->setExpanded(true); if (item->type() != "namespace") return; expandedNamespaces.insert(item->getFullPath()); } void Model::setCollapsed(const QModelIndex &index) { TreeItem *item = getItemFromIndex(index); if (!item) return; QTimer::singleShot(10, [item]() { if (item) item->setExpanded(false); }); if (item->type() != "namespace") return; if (expandedNamespaces.contains(item->getFullPath())) { expandedNamespaces.remove(item->getFullPath()); QMutableSetIterator it(expandedNamespaces); while (it.hasNext()) { if (it.next().startsWith(item->getFullPath())) it.remove(); } } } void Model::collapseRootItems() { for (const auto &item : qAsConst(m_treeItems)) { auto server = item.dynamicCast(); if (!server) continue; server->unload(); } } void Model::dropItemAt(const QModelIndex &index, const QModelIndex &at) { if (!(index.isValid() && at.isValid())) return; auto item = getItemFromIndex(index); auto targetItem = getItemFromIndex(at); if (!(item && targetItem)) return; if (!(item->type() == "server" && targetItem->type() == "server_group" && (!item->parent() || item->parent() != targetItem->getSelf()))) { return; } int targetIndex = targetItem->childCount(); auto srcParent = QModelIndex(); auto targetParent = at; if (item->parent()) { srcParent = getIndexFromItem(item->parent()); } bool res = beginMoveRows(srcParent, index.row(), index.row(), targetParent, targetIndex); if (!res) { return; } auto findRootItem = [](TreeItem* t, QList> treeItems) { for (auto rI : treeItems) { if (t == rI.data()) return rI; } return QSharedPointer(); }; QSharedPointer srv; if (item->parent()) { srv = findRootItem(item, item->parent().toStrongRef()->getAllChilds()); auto sourceGroup = item->parent().toStrongRef().dynamicCast(); if (!sourceGroup) { qDebug() << "invalid source group"; return; } sourceGroup->removeConnection(srv); } else { srv = findRootItem(item, m_treeItems); m_treeItems.removeAll(srv); } endMoveRows(); auto targetGroup = findRootItem(targetItem, m_treeItems).dynamicCast(); if (!targetGroup) { qDebug() << "invalid target group"; return; } targetGroup->addServer(srv); auto srvItem = srv.dynamicCast(); if (!srvItem) { qDebug() << "invalid srv item"; return; } srvItem->setParent(targetGroup.toWeakRef()); emit layoutAboutToBeChanged(); emit layoutChanged(); } void Model::applyGroupChanges() { emit layoutAboutToBeChanged(); // TBD emit layoutChanged(); } void Model::addRootItem(QSharedPointer item) { if (item.isNull()) return; int insertIndex = m_treeItems.size(); beginInsertRows(QModelIndex(), insertIndex, insertIndex); item->setRow(insertIndex); m_treeItems.push_back(item); endInsertRows(); if (item->isExpanded() && item->childCount() > 0) { QTimer::singleShot(100, this, [this, item]() { if (item) emit expand(getIndexFromItem(item)); }); } } void Model::removeRootItem(QSharedPointer item) { if (!item) return; beginRemoveRows(QModelIndex(), item->row(), item->row()); m_treeItems.removeAll(item); endRemoveRows(); } ================================================ FILE: src/modules/connections-tree/model.h ================================================ #pragma once #include #include #include #include #include #include #include "items/sortabletreeitem.h" namespace ConnectionsTree { class ServerItem; class AbstractNamespaceItem; class Model : public QAbstractItemModel { Q_OBJECT public: enum Roles { itemMetaData = Qt::UserRole + 1 }; public: explicit Model(QObject *parent = 0); QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QModelIndex index(int row, int column, const QModelIndex &parent) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; inline int columnCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent); return 1; } inline TreeItem *getItemFromIndex(const QModelIndex &index) const { if (!index.isValid()) return nullptr; if (index.model() != this) return nullptr; TreeItem *item = static_cast(index.internalPointer()); if (!item || !m_rawPointers->contains(item)) return nullptr; if (!m_rawPointers->value(item)) { m_rawPointers->remove(item); return nullptr; } return item; } QModelIndex getIndexFromItem(QWeakPointer); QSet expandedNamespaces; signals: void expand(const QModelIndex &index); void error(const QString &err); void itemChanged(QWeakPointer item); public: void beforeItemChildsUnloaded(QWeakPointer item); void beforeChildLoadedAtPos(QWeakPointer item, int pos); void beforeChildLoaded(QWeakPointer item, int count); void childLoaded(QWeakPointer item); void beforeItemChildRemoved(QWeakPointer item, int row); void itemChildRemoved(QWeakPointer childItem); void expandItem(QWeakPointer item); void beforeItemLayoutChanged(QWeakPointer item); void itemLayoutChanged(QWeakPointer item); public slots: void onItemChanged(QWeakPointer item); void setMetadata(const QModelIndex &index, const QString &metaKey, QVariant value); void sendEvent(const QModelIndex &index, QString event); virtual int size(); void setExpanded(const QModelIndex &index); void setCollapsed(const QModelIndex &index); void collapseRootItems(); void dropItemAt(const QModelIndex &index, const QModelIndex &at); virtual void applyGroupChanges(); protected: void addRootItem(QSharedPointer item); void removeRootItem(QSharedPointer item); typedef QPair, QModelIndex> PendingIndexChange; void iterateAllChilds(QSharedPointer item, QList &pendingChanges); protected: QList> m_treeItems; QSharedPointer>> m_rawPointers; QHash, QList> m_pendingChanges; }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/operations.h ================================================ #pragma once #include #include #include #include #include #include #include #include "exception.h" #include "modules/common/callbackwithowner.h" namespace Console { class Operations; } namespace ConnectionsTree { class KeyItem; class NamespaceItem; class AbstractNamespaceItem; class DatabaseItem; class TreeItem; class Operations { ADD_EXCEPTION public: /** * List of databases with keys counters * @emit databesesLoaded **/ using DbMapping = QMap; using GetDatabasesCallback = CallbackWithOwner; virtual QFuture getDatabases(QSharedPointer) = 0; /** * @brief loadNamespaceItems * @param dbIndex * @param filter * @param callback */ using LoadNamespaceItemsCallback = CallbackWithOwner; virtual void loadNamespaceItems(uint dbIndex, const QString& filter, QSharedPointer) = 0; /** * Cancel all operations & close connection * @brief disconnect */ virtual void disconnect() = 0; /** Cancel all operations & reconnect * @brief resetConnection */ virtual void resetConnection() = 0; /** * @brief getNamespaceSeparator * @return */ virtual QString getNamespaceSeparator() = 0; virtual QString iconColor() = 0; virtual QString defaultFilter() = 0; virtual QVariantMap getFilterHistory() = 0; virtual QString connectionName() const = 0; virtual void openKeyTab(QSharedPointer key, bool openInNewTab) = 0; virtual void openConsoleTab(int dbIndex = 0) = 0; using OpenNewKeyDialogCallback = CallbackWithOwner; virtual void openNewKeyDialog(int dbIndex, QSharedPointer callback, QString keyPrefix = QString()) = 0; virtual void openServerStats() = 0; virtual void duplicateConnection() = 0; virtual void notifyDbWasUnloaded(int dbIndex) = 0; using DeleteDbKeyCallback = CallbackWithOwner; virtual void deleteDbKey(ConnectionsTree::KeyItem& key, QSharedPointer callback) = 0; virtual void deleteDbKeys(ConnectionsTree::DatabaseItem& db) = 0; virtual void deleteDbNamespace(ConnectionsTree::NamespaceItem& ns) = 0; virtual void setTTL(ConnectionsTree::AbstractNamespaceItem& ns) = 0; virtual void copyKeys(ConnectionsTree::AbstractNamespaceItem& ns) = 0; virtual void importKeysFromRdb(ConnectionsTree::DatabaseItem& ns) = 0; using FlushDbCallback = CallbackWithOwner; virtual void flushDb(int dbIndex, QSharedPointer callback) = 0; using OpenKeyIfExistsCallback = CallbackWithOwner; virtual void openKeyIfExists( const QByteArray& key, QSharedPointer parent, QSharedPointer callback) = 0; virtual QString mode() = 0; virtual bool isConnected() const = 0; virtual QFuture connectionSupportsMemoryOperations() = 0; using GetUsedMemoryCallback = CallbackWithOwner; virtual void getUsedMemory( const QList& keys, int dbIndex, QSharedPointer result, QSharedPointer progress) = 0; virtual ~Operations() {} }; } // namespace ConnectionsTree ================================================ FILE: src/modules/connections-tree/utils.cpp ================================================ #include "utils.h" #include #include #include void ConnectionsTree::confirmAction(QWidget *parent, const QString &msg, std::function action, QString title) { QMessageBox::StandardButton reply = QMessageBox::question(parent, title, msg, QMessageBox::Yes|QMessageBox::No); if (reply == QMessageBox::Yes) { action(); } } ================================================ FILE: src/modules/connections-tree/utils.h ================================================ #pragma once #include #include #include namespace ConnectionsTree { void confirmAction(QWidget* parent, const QString& msg, std::function action, QString title = "Confirm action"); } ================================================ FILE: src/modules/console/autocompletemodel.cpp ================================================ #include "autocompletemodel.h" #include #include #include #include #include Console::AutocompleteModel::AutocompleteModel() { QFile commandsResource("://commands.json"); if (!commandsResource.open(QIODevice::ReadOnly)) { qWarning() << "Cannot load list of redis commands. Autocomplete in redis console wont work."; return; } QByteArray commandsJsonRaw = commandsResource.readAll(); QJsonDocument commandsJson = QJsonDocument::fromJson(commandsJsonRaw); if (commandsJson.isEmpty() || !commandsJson.isArray()) { qWarning() << "Invalid commands.json" << commandsJson; return; } auto commands = commandsJson.array(); for (auto command : commands) { auto cmd = command.toObject(); m_commands.append( CommandInfo { cmd["cmd"].toString(), cmd["arguments"].toString(), cmd["summary"].toString(), cmd["since"].toString() } ); } } QModelIndex Console::AutocompleteModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); if (row < 0 || column < 0) return QModelIndex(); return createIndex(row, 0); } int Console::AutocompleteModel::rowCount(const QModelIndex &parent) const { return m_commands.size(); } QVariant Console::AutocompleteModel::data(const QModelIndex &index, int role) const { if (!isIndexValid(index)) return QVariant(); auto command = m_commands[index.row()]; switch (role) { case name: return command.name; case arguments: return command.arguments; case summary: return command.summary; case since: return command.since; } return QVariant(); } QHash Console::AutocompleteModel::roleNames() const { QHash roles; roles[name] = "name"; roles[arguments] = "arguments"; roles[summary] = "summary"; roles[since] = "since"; return roles; } QVariantMap Console::AutocompleteModel::getRow(int i) { if (!isRowIndexValid(i)) return QVariantMap(); return getRowRaw(i); } ================================================ FILE: src/modules/console/autocompletemodel.h ================================================ #pragma once #include #include #include "common/baselistmodel.h" namespace Console { class AutocompleteModel : public BaseListModel { Q_OBJECT public: enum Roles { name = Qt::UserRole + 1, arguments, summary, since }; AutocompleteModel(); QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; Q_INVOKABLE QVariantMap getRow(int i); private: struct CommandInfo { QString name; QString arguments; QString summary; QString since; }; QList m_commands; }; } ================================================ FILE: src/modules/console/consolemodel.cpp ================================================ #include "consolemodel.h" #include #include using namespace Console; Model::Model(QSharedPointer connection, int dbIndex, QList initCmd) : TabModel(connection, dbIndex), m_current_db(dbIndex) { QObject::connect(this, &TabModel::initialized, [this, initCmd]() { if (m_connection->mode() == RedisClient::Connection::Mode::Cluster) { emit addOutput( QCoreApplication::translate("RESP", "Connected to cluster.\n"), "complete"); } else { emit addOutput(QCoreApplication::translate("RESP", "Connected.\n"), "complete"); } updatePrompt(true); if (initCmd.size() > 0) execCmd(initCmd); }); QObject::connect(this, &TabModel::error, [this](const QString& msg) { emit addOutput(msg, "error"); }); } QString Model::getName() const { return m_connection->getConfig().name(); } void Model::executeCommand(const QString& cmd) { return execCmd(RedisClient::Command::splitCommandString(cmd)); } void Model::updatePrompt(bool showPrompt) { if (m_connection->mode() == RedisClient::Connection::Mode::Cluster) { emit changePrompt(QString("%1(%2:%3)>") .arg(m_connection->getConfig().name()) .arg(m_connection->getConfig().host()) .arg(m_connection->getConfig().port()), showPrompt); } else { emit changePrompt(QString("%1:%2>") .arg(m_connection->getConfig().name()) .arg(m_current_db), showPrompt); } } void Model::execCmd(QList cmd) { using namespace RedisClient; Command command(cmd, m_current_db); if (command.isSubscriptionCommand() || command.isMonitorCommand()) { emit addOutput( QCoreApplication::translate( "RESP", "Switch to %1 mode. Close console tab to stop listen for " "messages.").arg(command.isSubscriptionCommand()? "Pub/Sub": "Monitor"), "part"); command.setCallBack(this, [this](Response result, QString err) { if (!err.isEmpty()) { emit addOutput( QCoreApplication::translate("RESP", "Subscribe error: %1").arg(err), "error"); return; } QVariant value = result.value(); emit addOutput(RedisClient::Response::valueToHumanReadString(value).replace("\r\n", "\n"), "part"); }); m_connection->command(command); } else { bool isSelect = command.isSelectCommand(); command.setCallBack(this, [this, isSelect](Response result, QString err) { if (!err.isEmpty()) { emit addOutput(QCoreApplication::translate("RESP", "Connection error: ") + QString(err), "error"); return; } if (isSelect || m_connection->mode() == RedisClient::Connection::Mode::Cluster) { m_current_db = m_connection->dbIndex(); updatePrompt(false); } QVariant value = result.value(); emit addOutput(RedisClient::Response::valueToHumanReadString(value).replace("\r\n", "\n"), "complete"); }); m_connection->command(command); } } ================================================ FILE: src/modules/console/consolemodel.h ================================================ #pragma once #include "common/tabviewmodel.h" #include "exception.h" namespace Console { class Model : public TabModel { Q_OBJECT ADD_EXCEPTION public: Model(QSharedPointer connection, int dbIndex, QList initCmd); QString getName() const override; public slots: void executeCommand(const QString &); signals: void changePrompt(const QString &text, bool showPrompt); void addOutput(const QString &text, QString resultType); private: int m_current_db; private: void updatePrompt(bool showPrompt); void execCmd(QList cmd); }; } // namespace Console ================================================ FILE: src/modules/exception.h ================================================ #pragma once #include #include #define ADD_EXCEPTION \ public: struct Exception : public std::runtime_error { \ Exception(const QString &err) : std::runtime_error(err.toStdString()) {} \ }; ================================================ FILE: src/modules/extension-server/client/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.2) project(client) set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) if (MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") else () set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wall -Wno-unused-variable") endif () find_package(Qt5Core REQUIRED) find_package(Qt5Network REQUIRED) add_library(${PROJECT_NAME} OAIDataFormatter.cpp OAIDecodePayload.cpp OAIEncodePayload.cpp OAIInline_response_400.cpp OAIDefaultApi.cpp OAIHelpers.cpp OAIHttpRequest.cpp OAIHttpFileElement.cpp OAIOauth.cpp ) target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Core Qt5::Network) if(NOT APPLE) target_link_libraries(${PROJECT_NAME} PRIVATE ssl crypto) endif() set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 14) set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD_REQUIRED ON) set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_EXTENSIONS OFF) install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib) ================================================ FILE: src/modules/extension-server/client/OAIDataFormatter.cpp ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #include "OAIDataFormatter.h" #include #include #include #include #include "OAIHelpers.h" namespace RespExtServer { OAIDataFormatter::OAIDataFormatter(QString json) { this->initializeModel(); this->fromJson(json); } OAIDataFormatter::OAIDataFormatter() { this->initializeModel(); } OAIDataFormatter::~OAIDataFormatter() {} void OAIDataFormatter::initializeModel() { m_id_isSet = false; m_id_isValid = false; m_name_isSet = false; m_name_isValid = false; m_key_types_isSet = false; m_key_types_isValid = false; m_magic_header_isSet = false; m_magic_header_isValid = false; m_read_only_isSet = false; m_read_only_isValid = false; } void OAIDataFormatter::fromJson(QString jsonString) { QByteArray array(jsonString.toStdString().c_str()); QJsonDocument doc = QJsonDocument::fromJson(array); QJsonObject jsonObject = doc.object(); this->fromJsonObject(jsonObject); } void OAIDataFormatter::fromJsonObject(QJsonObject json) { m_id_isValid = ::RespExtServer::fromJsonValue(id, json[QString("id")]); m_id_isSet = !json[QString("id")].isNull() && m_id_isValid; m_name_isValid = ::RespExtServer::fromJsonValue(name, json[QString("name")]); m_name_isSet = !json[QString("name")].isNull() && m_name_isValid; m_key_types_isValid = ::RespExtServer::fromJsonValue(key_types, json[QString("key-types")]); m_key_types_isSet = !json[QString("key-types")].isNull() && m_key_types_isValid; m_magic_header_isValid = ::RespExtServer::fromJsonValue(magic_header, json[QString("magic-header")]); m_magic_header_isSet = !json[QString("magic-header")].isNull() && m_magic_header_isValid; m_read_only_isValid = ::RespExtServer::fromJsonValue(read_only, json[QString("read-only")]); m_read_only_isSet = !json[QString("read-only")].isNull() && m_read_only_isValid; } QString OAIDataFormatter::asJson() const { QJsonObject obj = this->asJsonObject(); QJsonDocument doc(obj); QByteArray bytes = doc.toJson(); return QString(bytes); } QJsonObject OAIDataFormatter::asJsonObject() const { QJsonObject obj; if (m_id_isSet) { obj.insert(QString("id"), ::RespExtServer::toJsonValue(id)); } if (m_name_isSet) { obj.insert(QString("name"), ::RespExtServer::toJsonValue(name)); } if (m_key_types_isSet) { obj.insert(QString("key-types"), ::RespExtServer::toJsonValue(key_types)); } if (m_magic_header_isSet) { obj.insert(QString("magic-header"), ::RespExtServer::toJsonValue(magic_header)); } if (m_read_only_isSet) { obj.insert(QString("read-only"), ::RespExtServer::toJsonValue(read_only)); } return obj; } QString OAIDataFormatter::getId() const { return id; } void OAIDataFormatter::setId(const QString &id) { this->id = id; this->m_id_isSet = true; } bool OAIDataFormatter::is_id_Set() const{ return m_id_isSet; } bool OAIDataFormatter::is_id_Valid() const{ return m_id_isValid; } QString OAIDataFormatter::getName() const { return name; } void OAIDataFormatter::setName(const QString &name) { this->name = name; this->m_name_isSet = true; } bool OAIDataFormatter::is_name_Set() const{ return m_name_isSet; } bool OAIDataFormatter::is_name_Valid() const{ return m_name_isValid; } QString OAIDataFormatter::getKeyTypes() const { return key_types; } void OAIDataFormatter::setKeyTypes(const QString &key_types) { this->key_types = key_types; this->m_key_types_isSet = true; } bool OAIDataFormatter::is_key_types_Set() const{ return m_key_types_isSet; } bool OAIDataFormatter::is_key_types_Valid() const{ return m_key_types_isValid; } QString OAIDataFormatter::getMagicHeader() const { return magic_header; } void OAIDataFormatter::setMagicHeader(const QString &magic_header) { this->magic_header = magic_header; this->m_magic_header_isSet = true; } bool OAIDataFormatter::is_magic_header_Set() const{ return m_magic_header_isSet; } bool OAIDataFormatter::is_magic_header_Valid() const{ return m_magic_header_isValid; } bool OAIDataFormatter::isReadOnly() const { return read_only; } void OAIDataFormatter::setReadOnly(const bool &read_only) { this->read_only = read_only; this->m_read_only_isSet = true; } bool OAIDataFormatter::is_read_only_Set() const{ return m_read_only_isSet; } bool OAIDataFormatter::is_read_only_Valid() const{ return m_read_only_isValid; } bool OAIDataFormatter::isSet() const { bool isObjectUpdated = false; do { if (m_id_isSet) { isObjectUpdated = true; break; } if (m_name_isSet) { isObjectUpdated = true; break; } if (m_key_types_isSet) { isObjectUpdated = true; break; } if (m_magic_header_isSet) { isObjectUpdated = true; break; } if (m_read_only_isSet) { isObjectUpdated = true; break; } } while (false); return isObjectUpdated; } bool OAIDataFormatter::isValid() const { // only required properties are required for the object to be considered valid return m_id_isValid && m_name_isValid && true; } } // namespace RespExtServer ================================================ FILE: src/modules/extension-server/client/OAIDataFormatter.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /* * OAIDataFormatter.h * * */ #ifndef OAIDataFormatter_H #define OAIDataFormatter_H #include #include #include "OAIEnum.h" #include "OAIObject.h" namespace RespExtServer { class OAIDataFormatter : public OAIObject { public: OAIDataFormatter(); OAIDataFormatter(QString json); ~OAIDataFormatter() override; QString asJson() const override; QJsonObject asJsonObject() const override; void fromJsonObject(QJsonObject json) override; void fromJson(QString jsonString) override; QString getId() const; void setId(const QString &id); bool is_id_Set() const; bool is_id_Valid() const; QString getName() const; void setName(const QString &name); bool is_name_Set() const; bool is_name_Valid() const; QString getKeyTypes() const; void setKeyTypes(const QString &key_types); bool is_key_types_Set() const; bool is_key_types_Valid() const; QString getMagicHeader() const; void setMagicHeader(const QString &magic_header); bool is_magic_header_Set() const; bool is_magic_header_Valid() const; bool isReadOnly() const; void setReadOnly(const bool &read_only); bool is_read_only_Set() const; bool is_read_only_Valid() const; virtual bool isSet() const override; virtual bool isValid() const override; private: void initializeModel(); QString id; bool m_id_isSet; bool m_id_isValid; QString name; bool m_name_isSet; bool m_name_isValid; QString key_types; bool m_key_types_isSet; bool m_key_types_isValid; QString magic_header; bool m_magic_header_isSet; bool m_magic_header_isValid; bool read_only; bool m_read_only_isSet; bool m_read_only_isValid; }; } // namespace RespExtServer Q_DECLARE_METATYPE(RespExtServer::OAIDataFormatter) #endif // OAIDataFormatter_H ================================================ FILE: src/modules/extension-server/client/OAIDecodePayload.cpp ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #include "OAIDecodePayload.h" #include #include #include #include #include "OAIHelpers.h" namespace RespExtServer { OAIDecodePayload::OAIDecodePayload(QString json) { this->initializeModel(); this->fromJson(json); } OAIDecodePayload::OAIDecodePayload() { this->initializeModel(); } OAIDecodePayload::~OAIDecodePayload() {} void OAIDecodePayload::initializeModel() { m_data_isSet = false; m_data_isValid = false; m_redis_key_name_isSet = false; m_redis_key_name_isValid = false; m_redis_key_type_isSet = false; m_redis_key_type_isValid = false; } void OAIDecodePayload::fromJson(QString jsonString) { QByteArray array(jsonString.toStdString().c_str()); QJsonDocument doc = QJsonDocument::fromJson(array); QJsonObject jsonObject = doc.object(); this->fromJsonObject(jsonObject); } void OAIDecodePayload::fromJsonObject(QJsonObject json) { m_data_isValid = ::RespExtServer::fromJsonValue(data, json[QString("data")]); m_data_isSet = !json[QString("data")].isNull() && m_data_isValid; m_redis_key_name_isValid = ::RespExtServer::fromJsonValue(redis_key_name, json[QString("redis-key-name")]); m_redis_key_name_isSet = !json[QString("redis-key-name")].isNull() && m_redis_key_name_isValid; m_redis_key_type_isValid = ::RespExtServer::fromJsonValue(redis_key_type, json[QString("redis-key-type")]); m_redis_key_type_isSet = !json[QString("redis-key-type")].isNull() && m_redis_key_type_isValid; } QString OAIDecodePayload::asJson() const { QJsonObject obj = this->asJsonObject(); QJsonDocument doc(obj); QByteArray bytes = doc.toJson(); return QString(bytes); } QJsonObject OAIDecodePayload::asJsonObject() const { QJsonObject obj; if (m_data_isSet) { obj.insert(QString("data"), ::RespExtServer::toJsonValue(data)); } if (m_redis_key_name_isSet) { obj.insert(QString("redis-key-name"), ::RespExtServer::toJsonValue(redis_key_name)); } if (m_redis_key_type_isSet) { obj.insert(QString("redis-key-type"), ::RespExtServer::toJsonValue(redis_key_type)); } return obj; } QString OAIDecodePayload::getData() const { return data; } void OAIDecodePayload::setData(const QString &data) { this->data = data; this->m_data_isSet = true; } bool OAIDecodePayload::is_data_Set() const{ return m_data_isSet; } bool OAIDecodePayload::is_data_Valid() const{ return m_data_isValid; } QString OAIDecodePayload::getRedisKeyName() const { return redis_key_name; } void OAIDecodePayload::setRedisKeyName(const QString &redis_key_name) { this->redis_key_name = redis_key_name; this->m_redis_key_name_isSet = true; } bool OAIDecodePayload::is_redis_key_name_Set() const{ return m_redis_key_name_isSet; } bool OAIDecodePayload::is_redis_key_name_Valid() const{ return m_redis_key_name_isValid; } QString OAIDecodePayload::getRedisKeyType() const { return redis_key_type; } void OAIDecodePayload::setRedisKeyType(const QString &redis_key_type) { this->redis_key_type = redis_key_type; this->m_redis_key_type_isSet = true; } bool OAIDecodePayload::is_redis_key_type_Set() const{ return m_redis_key_type_isSet; } bool OAIDecodePayload::is_redis_key_type_Valid() const{ return m_redis_key_type_isValid; } bool OAIDecodePayload::isSet() const { bool isObjectUpdated = false; do { if (m_data_isSet) { isObjectUpdated = true; break; } if (m_redis_key_name_isSet) { isObjectUpdated = true; break; } if (m_redis_key_type_isSet) { isObjectUpdated = true; break; } } while (false); return isObjectUpdated; } bool OAIDecodePayload::isValid() const { // only required properties are required for the object to be considered valid return true; } } // namespace RespExtServer ================================================ FILE: src/modules/extension-server/client/OAIDecodePayload.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /* * OAIDecodePayload.h * * */ #ifndef OAIDecodePayload_H #define OAIDecodePayload_H #include #include #include "OAIEnum.h" #include "OAIObject.h" namespace RespExtServer { class OAIDecodePayload : public OAIObject { public: OAIDecodePayload(); OAIDecodePayload(QString json); ~OAIDecodePayload() override; QString asJson() const override; QJsonObject asJsonObject() const override; void fromJsonObject(QJsonObject json) override; void fromJson(QString jsonString) override; QString getData() const; void setData(const QString &data); bool is_data_Set() const; bool is_data_Valid() const; QString getRedisKeyName() const; void setRedisKeyName(const QString &redis_key_name); bool is_redis_key_name_Set() const; bool is_redis_key_name_Valid() const; QString getRedisKeyType() const; void setRedisKeyType(const QString &redis_key_type); bool is_redis_key_type_Set() const; bool is_redis_key_type_Valid() const; virtual bool isSet() const override; virtual bool isValid() const override; private: void initializeModel(); QString data; bool m_data_isSet; bool m_data_isValid; QString redis_key_name; bool m_redis_key_name_isSet; bool m_redis_key_name_isValid; QString redis_key_type; bool m_redis_key_type_isSet; bool m_redis_key_type_isValid; }; } // namespace RespExtServer Q_DECLARE_METATYPE(RespExtServer::OAIDecodePayload) #endif // OAIDecodePayload_H ================================================ FILE: src/modules/extension-server/client/OAIDefaultApi.cpp ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #include "OAIDefaultApi.h" #include "OAIServerConfiguration.h" #include #include namespace RespExtServer { OAIDefaultApi::OAIDefaultApi(const int timeOut) : _timeOut(timeOut), _manager(nullptr), _isResponseCompressionEnabled(false), _isRequestCompressionEnabled(false) { initializeServerConfigs(); } OAIDefaultApi::~OAIDefaultApi() { } void OAIDefaultApi::initializeServerConfigs() { //Default server QList defaultConf = QList(); //varying endpoint server defaultConf.append(OAIServerConfiguration( QUrl("/"), "No description provided", QMap())); _serverConfigs.insert("dataFormattersGet", defaultConf); _serverIndices.insert("dataFormattersGet", 0); _serverConfigs.insert("dataFormattersIdDecodePost", defaultConf); _serverIndices.insert("dataFormattersIdDecodePost", 0); _serverConfigs.insert("dataFormattersIdEncodePost", defaultConf); _serverIndices.insert("dataFormattersIdEncodePost", 0); } /** * returns 0 on success and -1, -2 or -3 on failure. * -1 when the variable does not exist and -2 if the value is not defined in the enum and -3 if the operation or server index is not found */ int OAIDefaultApi::setDefaultServerValue(int serverIndex, const QString &operation, const QString &variable, const QString &value) { auto it = _serverConfigs.find(operation); if (it != _serverConfigs.end() && serverIndex < it.value().size()) { return _serverConfigs[operation][serverIndex].setDefaultValue(variable,value); } return -3; } void OAIDefaultApi::setServerIndex(const QString &operation, int serverIndex) { if (_serverIndices.contains(operation) && serverIndex < _serverConfigs.find(operation).value().size()) { _serverIndices[operation] = serverIndex; } } void OAIDefaultApi::setApiKey(const QString &apiKeyName, const QString &apiKey) { _apiKeys.insert(apiKeyName,apiKey); } void OAIDefaultApi::setBearerToken(const QString &token) { _bearerToken = token; } void OAIDefaultApi::setUsername(const QString &username) { _username = username; } void OAIDefaultApi::setPassword(const QString &password) { _password = password; } void OAIDefaultApi::setTimeOut(const int timeOut) { _timeOut = timeOut; } void OAIDefaultApi::setWorkingDirectory(const QString &path) { _workingDirectory = path; } void OAIDefaultApi::setNetworkAccessManager(QNetworkAccessManager* manager) { _manager = manager; } /** * Appends a new ServerConfiguration to the config map for a specific operation. * @param operation The id to the target operation. * @param url A string that contains the URL of the server * @param description A String that describes the server * @param variables A map between a variable name and its value. The value is used for substitution in the server's URL template. * returns the index of the new server config on success and -1 if the operation is not found */ int OAIDefaultApi::addServerConfiguration(const QString &operation, const QUrl &url, const QString &description, const QMap &variables) { if (_serverConfigs.contains(operation)) { _serverConfigs[operation].append(OAIServerConfiguration( url, description, variables)); return _serverConfigs[operation].size()-1; } else { return -1; } } /** * Appends a new ServerConfiguration to the config map for a all operations and sets the index to that server. * @param url A string that contains the URL of the server * @param description A String that describes the server * @param variables A map between a variable name and its value. The value is used for substitution in the server's URL template. */ void OAIDefaultApi::setNewServerForAllOperations(const QUrl &url, const QString &description, const QMap &variables) { #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) for (auto keyIt = _serverIndices.keyBegin(); keyIt != _serverIndices.keyEnd(); keyIt++) { setServerIndex(*keyIt, addServerConfiguration(*keyIt, url, description, variables)); } #else for (auto &e : _serverIndices.keys()) { setServerIndex(e, addServerConfiguration(e, url, description, variables)); } #endif } /** * Appends a new ServerConfiguration to the config map for an operations and sets the index to that server. * @param URL A string that contains the URL of the server * @param description A String that describes the server * @param variables A map between a variable name and its value. The value is used for substitution in the server's URL template. */ void OAIDefaultApi::setNewServer(const QString &operation, const QUrl &url, const QString &description, const QMap &variables) { setServerIndex(operation, addServerConfiguration(operation, url, description, variables)); } void OAIDefaultApi::addHeaders(const QString &key, const QString &value) { _defaultHeaders.insert(key, value); } void OAIDefaultApi::enableRequestCompression() { _isRequestCompressionEnabled = true; } void OAIDefaultApi::enableResponseCompression() { _isResponseCompressionEnabled = true; } void OAIDefaultApi::abortRequests() { emit abortRequestsSignal(); } QString OAIDefaultApi::getParamStylePrefix(const QString &style) { if (style == "matrix") { return ";"; } else if (style == "label") { return "."; } else if (style == "form") { return "&"; } else if (style == "simple") { return ""; } else if (style == "spaceDelimited") { return "&"; } else if (style == "pipeDelimited") { return "&"; } else { return "none"; } } QString OAIDefaultApi::getParamStyleSuffix(const QString &style) { if (style == "matrix") { return "="; } else if (style == "label") { return ""; } else if (style == "form") { return "="; } else if (style == "simple") { return ""; } else if (style == "spaceDelimited") { return "="; } else if (style == "pipeDelimited") { return "="; } else { return "none"; } } QString OAIDefaultApi::getParamStyleDelimiter(const QString &style, const QString &name, bool isExplode) { if (style == "matrix") { return (isExplode) ? ";" + name + "=" : ","; } else if (style == "label") { return (isExplode) ? "." : ","; } else if (style == "form") { return (isExplode) ? "&" + name + "=" : ","; } else if (style == "simple") { return ","; } else if (style == "spaceDelimited") { return (isExplode) ? "&" + name + "=" : " "; } else if (style == "pipeDelimited") { return (isExplode) ? "&" + name + "=" : "|"; } else if (style == "deepObject") { return (isExplode) ? "&" : "none"; } else { return "none"; } } void OAIDefaultApi::dataFormattersGet() { QString fullPath = QString(_serverConfigs["dataFormattersGet"][_serverIndices.value("dataFormattersGet")].URL()+"/data-formatters"); if (!_username.isEmpty() && !_password.isEmpty()) { QByteArray b64; b64.append(_username.toUtf8() + ":" + _password.toUtf8()); addHeaders("Authorization","Basic " + b64.toBase64()); } OAIHttpRequestWorker *worker = new OAIHttpRequestWorker(this, _manager); worker->setTimeOut(_timeOut); worker->setWorkingDirectory(_workingDirectory); OAIHttpRequestInput input(fullPath, "GET"); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) for (auto keyValueIt = _defaultHeaders.keyValueBegin(); keyValueIt != _defaultHeaders.keyValueEnd(); keyValueIt++) { input.headers.insert(keyValueIt->first, keyValueIt->second); } #else for (auto key : _defaultHeaders.keys()) { input.headers.insert(key, _defaultHeaders[key]); } #endif connect(worker, &OAIHttpRequestWorker::on_execution_finished, this, &OAIDefaultApi::dataFormattersGetCallback); connect(this, &OAIDefaultApi::abortRequestsSignal, worker, &QObject::deleteLater); connect(worker, &QObject::destroyed, this, [this]() { if (findChildren().count() == 0) { emit allPendingRequestsCompleted(); } }); worker->execute(&input); } void OAIDefaultApi::dataFormattersGetCallback(OAIHttpRequestWorker *worker) { QString error_str = worker->error_str; QNetworkReply::NetworkError error_type = worker->error_type; if (worker->error_type != QNetworkReply::NoError) { error_str = QString("%1, %2").arg(worker->error_str, QString(worker->response)); } QList output; QString json(worker->response); QByteArray array(json.toStdString().c_str()); QJsonDocument doc = QJsonDocument::fromJson(array); QJsonArray jsonArray = doc.array(); foreach (QJsonValue obj, jsonArray) { OAIDataFormatter val; ::RespExtServer::fromJsonValue(val, obj); output.append(val); } worker->deleteLater(); if (worker->error_type == QNetworkReply::NoError) { emit dataFormattersGetSignal(output); emit dataFormattersGetSignalFull(worker, output); } else { emit dataFormattersGetSignalE(output, error_type, error_str); emit dataFormattersGetSignalEFull(worker, error_type, error_str); } } void OAIDefaultApi::dataFormattersIdDecodePost(const QString &id, const ::RespExtServer::OptionalParam &oai_decode_payload) { QString fullPath = QString(_serverConfigs["dataFormattersIdDecodePost"][_serverIndices.value("dataFormattersIdDecodePost")].URL()+"/data-formatters/{id}/decode"); if (!_username.isEmpty() && !_password.isEmpty()) { QByteArray b64; b64.append(_username.toUtf8() + ":" + _password.toUtf8()); addHeaders("Authorization","Basic " + b64.toBase64()); } { QString idPathParam("{"); idPathParam.append("id").append("}"); QString pathPrefix, pathSuffix, pathDelimiter; QString pathStyle = "simple"; if (pathStyle == "") pathStyle = "simple"; pathPrefix = getParamStylePrefix(pathStyle); pathSuffix = getParamStyleSuffix(pathStyle); pathDelimiter = getParamStyleDelimiter(pathStyle, "id", false); QString paramString = (pathStyle == "matrix") ? pathPrefix+"id"+pathSuffix : pathPrefix; fullPath.replace(idPathParam, paramString+QUrl::toPercentEncoding(::RespExtServer::toStringValue(id))); } OAIHttpRequestWorker *worker = new OAIHttpRequestWorker(this, _manager); worker->setTimeOut(_timeOut); worker->setWorkingDirectory(_workingDirectory); OAIHttpRequestInput input(fullPath, "POST"); if (oai_decode_payload.hasValue()){ QByteArray output = oai_decode_payload.value().asJson().toUtf8(); input.request_body.append(output); } #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) for (auto keyValueIt = _defaultHeaders.keyValueBegin(); keyValueIt != _defaultHeaders.keyValueEnd(); keyValueIt++) { input.headers.insert(keyValueIt->first, keyValueIt->second); } #else for (auto key : _defaultHeaders.keys()) { input.headers.insert(key, _defaultHeaders[key]); } #endif connect(worker, &OAIHttpRequestWorker::on_execution_finished, this, &OAIDefaultApi::dataFormattersIdDecodePostCallback); connect(this, &OAIDefaultApi::abortRequestsSignal, worker, &QObject::deleteLater); connect(worker, &QObject::destroyed, this, [this]() { if (findChildren().count() == 0) { emit allPendingRequestsCompleted(); } }); worker->execute(&input); } void OAIDefaultApi::dataFormattersIdDecodePostCallback(OAIHttpRequestWorker *worker) { QString error_str = worker->error_str; QNetworkReply::NetworkError error_type = worker->error_type; if (worker->error_type != QNetworkReply::NoError) { error_str = QString("%1, %2").arg(worker->error_str, QString(worker->response)); } QString output; ::RespExtServer::fromStringValue(QString(worker->response), output); worker->deleteLater(); if (worker->error_type == QNetworkReply::NoError) { emit dataFormattersIdDecodePostSignal(output); emit dataFormattersIdDecodePostSignalFull(worker, output); } else { emit dataFormattersIdDecodePostSignalE(output, error_type, error_str); emit dataFormattersIdDecodePostSignalEFull(worker, error_type, error_str); } } void OAIDefaultApi::dataFormattersIdEncodePost(const QString &id, const ::RespExtServer::OptionalParam &oai_encode_payload) { QString fullPath = QString(_serverConfigs["dataFormattersIdEncodePost"][_serverIndices.value("dataFormattersIdEncodePost")].URL()+"/data-formatters/{id}/encode"); if (!_username.isEmpty() && !_password.isEmpty()) { QByteArray b64; b64.append(_username.toUtf8() + ":" + _password.toUtf8()); addHeaders("Authorization","Basic " + b64.toBase64()); } { QString idPathParam("{"); idPathParam.append("id").append("}"); QString pathPrefix, pathSuffix, pathDelimiter; QString pathStyle = "simple"; if (pathStyle == "") pathStyle = "simple"; pathPrefix = getParamStylePrefix(pathStyle); pathSuffix = getParamStyleSuffix(pathStyle); pathDelimiter = getParamStyleDelimiter(pathStyle, "id", false); QString paramString = (pathStyle == "matrix") ? pathPrefix+"id"+pathSuffix : pathPrefix; fullPath.replace(idPathParam, paramString+QUrl::toPercentEncoding(::RespExtServer::toStringValue(id))); } OAIHttpRequestWorker *worker = new OAIHttpRequestWorker(this, _manager); worker->setTimeOut(_timeOut); worker->setWorkingDirectory(_workingDirectory); OAIHttpRequestInput input(fullPath, "POST"); if (oai_encode_payload.hasValue()){ QByteArray output = oai_encode_payload.value().asJson().toUtf8(); input.request_body.append(output); } #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) for (auto keyValueIt = _defaultHeaders.keyValueBegin(); keyValueIt != _defaultHeaders.keyValueEnd(); keyValueIt++) { input.headers.insert(keyValueIt->first, keyValueIt->second); } #else for (auto key : _defaultHeaders.keys()) { input.headers.insert(key, _defaultHeaders[key]); } #endif connect(worker, &OAIHttpRequestWorker::on_execution_finished, this, &OAIDefaultApi::dataFormattersIdEncodePostCallback); connect(this, &OAIDefaultApi::abortRequestsSignal, worker, &QObject::deleteLater); connect(worker, &QObject::destroyed, this, [this]() { if (findChildren().count() == 0) { emit allPendingRequestsCompleted(); } }); worker->execute(&input); } void OAIDefaultApi::dataFormattersIdEncodePostCallback(OAIHttpRequestWorker *worker) { QString error_str = worker->error_str; QNetworkReply::NetworkError error_type = worker->error_type; if (worker->error_type != QNetworkReply::NoError) { error_str = QString("%1, %2").arg(worker->error_str, QString(worker->response)); } QString output; ::RespExtServer::fromStringValue(QString(worker->response), output); worker->deleteLater(); if (worker->error_type == QNetworkReply::NoError) { emit dataFormattersIdEncodePostSignal(output); emit dataFormattersIdEncodePostSignalFull(worker, output); } else { emit dataFormattersIdEncodePostSignalE(output, error_type, error_str); emit dataFormattersIdEncodePostSignalEFull(worker, error_type, error_str); } } void OAIDefaultApi::tokenAvailable(){ oauthToken token; switch (_OauthMethod) { case 1: //implicit flow token = _implicitFlow.getToken(_latestScope.join(" ")); if(token.isValid()){ _latestInput.headers.insert("Authorization", "Bearer " + token.getToken()); _latestWorker->execute(&_latestInput); }else{ _implicitFlow.removeToken(_latestScope.join(" ")); qDebug() << "Could not retreive a valid token"; } break; case 2: //authorization flow token = _authFlow.getToken(_latestScope.join(" ")); if(token.isValid()){ _latestInput.headers.insert("Authorization", "Bearer " + token.getToken()); _latestWorker->execute(&_latestInput); }else{ _authFlow.removeToken(_latestScope.join(" ")); qDebug() << "Could not retreive a valid token"; } break; case 3: //client credentials flow token = _credentialFlow.getToken(_latestScope.join(" ")); if(token.isValid()){ _latestInput.headers.insert("Authorization", "Bearer " + token.getToken()); _latestWorker->execute(&_latestInput); }else{ _credentialFlow.removeToken(_latestScope.join(" ")); qDebug() << "Could not retreive a valid token"; } break; case 4: //resource owner password flow token = _passwordFlow.getToken(_latestScope.join(" ")); if(token.isValid()){ _latestInput.headers.insert("Authorization", "Bearer " + token.getToken()); _latestWorker->execute(&_latestInput); }else{ _credentialFlow.removeToken(_latestScope.join(" ")); qDebug() << "Could not retreive a valid token"; } break; default: qDebug() << "No Oauth method set!"; break; } } } // namespace RespExtServer ================================================ FILE: src/modules/extension-server/client/OAIDefaultApi.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #ifndef OAI_OAIDefaultApi_H #define OAI_OAIDefaultApi_H #include "OAIHelpers.h" #include "OAIHttpRequest.h" #include "OAIServerConfiguration.h" #include "OAIOauth.h" #include "OAIDataFormatter.h" #include "OAIDecodePayload.h" #include "OAIEncodePayload.h" #include "OAIInline_response_400.h" #include #include #include #include #include #include namespace RespExtServer { class OAIDefaultApi : public QObject { Q_OBJECT public: OAIDefaultApi(const int timeOut = 0); ~OAIDefaultApi(); void initializeServerConfigs(); int setDefaultServerValue(int serverIndex,const QString &operation, const QString &variable,const QString &val); void setServerIndex(const QString &operation, int serverIndex); void setApiKey(const QString &apiKeyName, const QString &apiKey); void setBearerToken(const QString &token); void setUsername(const QString &username); void setPassword(const QString &password); void setTimeOut(const int timeOut); void setWorkingDirectory(const QString &path); void setNetworkAccessManager(QNetworkAccessManager* manager); int addServerConfiguration(const QString &operation, const QUrl &url, const QString &description = "", const QMap &variables = QMap()); void setNewServerForAllOperations(const QUrl &url, const QString &description = "", const QMap &variables = QMap()); void setNewServer(const QString &operation, const QUrl &url, const QString &description = "", const QMap &variables = QMap()); void addHeaders(const QString &key, const QString &value); void enableRequestCompression(); void enableResponseCompression(); void abortRequests(); QString getParamStylePrefix(const QString &style); QString getParamStyleSuffix(const QString &style); QString getParamStyleDelimiter(const QString &style, const QString &name, bool isExplode); void dataFormattersGet(); /** * @param[in] id QString [required] * @param[in] oai_decode_payload OAIDecodePayload [optional] */ void dataFormattersIdDecodePost(const QString &id, const ::RespExtServer::OptionalParam &oai_decode_payload = ::RespExtServer::OptionalParam()); /** * @param[in] id QString [required] * @param[in] oai_encode_payload OAIEncodePayload [optional] */ void dataFormattersIdEncodePost(const QString &id, const ::RespExtServer::OptionalParam &oai_encode_payload = ::RespExtServer::OptionalParam()); private: QMap _serverIndices; QMap> _serverConfigs; QMap _apiKeys; QString _bearerToken; QString _username; QString _password; int _timeOut; QString _workingDirectory; QNetworkAccessManager* _manager; QMap _defaultHeaders; bool _isResponseCompressionEnabled; bool _isRequestCompressionEnabled; OAIHttpRequestInput _latestInput; OAIHttpRequestWorker *_latestWorker; QStringList _latestScope; OauthCode _authFlow; OauthImplicit _implicitFlow; OauthCredentials _credentialFlow; OauthPassword _passwordFlow; int _OauthMethod = 0; void dataFormattersGetCallback(OAIHttpRequestWorker *worker); void dataFormattersIdDecodePostCallback(OAIHttpRequestWorker *worker); void dataFormattersIdEncodePostCallback(OAIHttpRequestWorker *worker); signals: void dataFormattersGetSignal(QList summary); void dataFormattersIdDecodePostSignal(QString summary); void dataFormattersIdEncodePostSignal(QString summary); void dataFormattersGetSignalFull(OAIHttpRequestWorker *worker, QList summary); void dataFormattersIdDecodePostSignalFull(OAIHttpRequestWorker *worker, QString summary); void dataFormattersIdEncodePostSignalFull(OAIHttpRequestWorker *worker, QString summary); void dataFormattersGetSignalE(QList summary, QNetworkReply::NetworkError error_type, QString error_str); void dataFormattersIdDecodePostSignalE(QString summary, QNetworkReply::NetworkError error_type, QString error_str); void dataFormattersIdEncodePostSignalE(QString summary, QNetworkReply::NetworkError error_type, QString error_str); void dataFormattersGetSignalEFull(OAIHttpRequestWorker *worker, QNetworkReply::NetworkError error_type, QString error_str); void dataFormattersIdDecodePostSignalEFull(OAIHttpRequestWorker *worker, QNetworkReply::NetworkError error_type, QString error_str); void dataFormattersIdEncodePostSignalEFull(OAIHttpRequestWorker *worker, QNetworkReply::NetworkError error_type, QString error_str); void abortRequestsSignal(); void allPendingRequestsCompleted(); public slots: void tokenAvailable(); }; } // namespace RespExtServer #endif ================================================ FILE: src/modules/extension-server/client/OAIEncodePayload.cpp ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #include "OAIEncodePayload.h" #include #include #include #include #include "OAIHelpers.h" namespace RespExtServer { OAIEncodePayload::OAIEncodePayload(QString json) { this->initializeModel(); this->fromJson(json); } OAIEncodePayload::OAIEncodePayload() { this->initializeModel(); } OAIEncodePayload::~OAIEncodePayload() {} void OAIEncodePayload::initializeModel() { m_data_isSet = false; m_data_isValid = false; m_metadata_isSet = false; m_metadata_isValid = false; } void OAIEncodePayload::fromJson(QString jsonString) { QByteArray array(jsonString.toStdString().c_str()); QJsonDocument doc = QJsonDocument::fromJson(array); QJsonObject jsonObject = doc.object(); this->fromJsonObject(jsonObject); } void OAIEncodePayload::fromJsonObject(QJsonObject json) { m_data_isValid = ::RespExtServer::fromJsonValue(data, json[QString("data")]); m_data_isSet = !json[QString("data")].isNull() && m_data_isValid; m_metadata_isValid = ::RespExtServer::fromJsonValue(metadata, json[QString("metadata")]); m_metadata_isSet = !json[QString("metadata")].isNull() && m_metadata_isValid; } QString OAIEncodePayload::asJson() const { QJsonObject obj = this->asJsonObject(); QJsonDocument doc(obj); QByteArray bytes = doc.toJson(); return QString(bytes); } QJsonObject OAIEncodePayload::asJsonObject() const { QJsonObject obj; if (m_data_isSet) { obj.insert(QString("data"), ::RespExtServer::toJsonValue(data)); } if (m_metadata_isSet) { obj.insert(QString("metadata"), ::RespExtServer::toJsonValue(metadata)); } return obj; } QString OAIEncodePayload::getData() const { return data; } void OAIEncodePayload::setData(const QString &data) { this->data = data; this->m_data_isSet = true; } bool OAIEncodePayload::is_data_Set() const{ return m_data_isSet; } bool OAIEncodePayload::is_data_Valid() const{ return m_data_isValid; } OAIObject OAIEncodePayload::getMetadata() const { return metadata; } void OAIEncodePayload::setMetadata(const OAIObject &metadata) { this->metadata = metadata; this->m_metadata_isSet = true; } bool OAIEncodePayload::is_metadata_Set() const{ return m_metadata_isSet; } bool OAIEncodePayload::is_metadata_Valid() const{ return m_metadata_isValid; } bool OAIEncodePayload::isSet() const { bool isObjectUpdated = false; do { if (m_data_isSet) { isObjectUpdated = true; break; } if (m_metadata_isSet) { isObjectUpdated = true; break; } } while (false); return isObjectUpdated; } bool OAIEncodePayload::isValid() const { // only required properties are required for the object to be considered valid return true; } } // namespace RespExtServer ================================================ FILE: src/modules/extension-server/client/OAIEncodePayload.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /* * OAIEncodePayload.h * * */ #ifndef OAIEncodePayload_H #define OAIEncodePayload_H #include #include "OAIObject.h" #include #include "OAIEnum.h" #include "OAIObject.h" namespace RespExtServer { class OAIEncodePayload : public OAIObject { public: OAIEncodePayload(); OAIEncodePayload(QString json); ~OAIEncodePayload() override; QString asJson() const override; QJsonObject asJsonObject() const override; void fromJsonObject(QJsonObject json) override; void fromJson(QString jsonString) override; QString getData() const; void setData(const QString &data); bool is_data_Set() const; bool is_data_Valid() const; OAIObject getMetadata() const; void setMetadata(const OAIObject &metadata); bool is_metadata_Set() const; bool is_metadata_Valid() const; virtual bool isSet() const override; virtual bool isValid() const override; private: void initializeModel(); QString data; bool m_data_isSet; bool m_data_isValid; OAIObject metadata; bool m_metadata_isSet; bool m_metadata_isValid; }; } // namespace RespExtServer Q_DECLARE_METATYPE(RespExtServer::OAIEncodePayload) #endif // OAIEncodePayload_H ================================================ FILE: src/modules/extension-server/client/OAIEnum.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #ifndef OAI_ENUM_H #define OAI_ENUM_H #include #include #include namespace RespExtServer { class OAIEnum { public: OAIEnum() {} OAIEnum(QString jsonString) { fromJson(jsonString); } virtual ~OAIEnum() {} virtual QJsonValue asJsonValue() const { return QJsonValue(jstr); } virtual QString asJson() const { return jstr; } virtual void fromJson(QString jsonString) { jstr = jsonString; } virtual void fromJsonValue(QJsonValue jval) { jstr = jval.toString(); } virtual bool isSet() const { return false; } virtual bool isValid() const { return true; } private: QString jstr; }; } // namespace RespExtServer Q_DECLARE_METATYPE(RespExtServer::OAIEnum) #endif // OAI_ENUM_H ================================================ FILE: src/modules/extension-server/client/OAIHelpers.cpp ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #include #include #include "OAIHelpers.h" namespace RespExtServer { class OAISerializerSettings { public: struct CustomDateTimeFormat{ bool isStringSet = false; QString formatString; bool isEnumSet = false; Qt::DateFormat formatEnum; }; static CustomDateTimeFormat getCustomDateTimeFormat() { return getInstance()->customDateTimeFormat; } static void setDateTimeFormatString(const QString &dtFormat){ getInstance()->customDateTimeFormat.isStringSet = true; getInstance()->customDateTimeFormat.isEnumSet = false; getInstance()->customDateTimeFormat.formatString = dtFormat; } static void setDateTimeFormatEnum(const Qt::DateFormat &dtFormat){ getInstance()->customDateTimeFormat.isEnumSet = true; getInstance()->customDateTimeFormat.isStringSet = false; getInstance()->customDateTimeFormat.formatEnum = dtFormat; } static OAISerializerSettings *getInstance(){ if(instance == nullptr){ instance = new OAISerializerSettings(); } return instance; } private: explicit OAISerializerSettings(){ instance = this; customDateTimeFormat.isStringSet = false; customDateTimeFormat.isEnumSet = false; } static OAISerializerSettings *instance; CustomDateTimeFormat customDateTimeFormat; }; OAISerializerSettings * OAISerializerSettings::instance = nullptr; bool setDateTimeFormat(const QString &dateTimeFormat){ bool success = false; auto dt = QDateTime::fromString(QDateTime::currentDateTime().toString(dateTimeFormat), dateTimeFormat); if (dt.isValid()) { success = true; OAISerializerSettings::setDateTimeFormatString(dateTimeFormat); } return success; } bool setDateTimeFormat(const Qt::DateFormat &dateTimeFormat){ bool success = false; auto dt = QDateTime::fromString(QDateTime::currentDateTime().toString(dateTimeFormat), dateTimeFormat); if (dt.isValid()) { success = true; OAISerializerSettings::setDateTimeFormatEnum(dateTimeFormat); } return success; } QString toStringValue(const QString &value) { return value; } QString toStringValue(const QDateTime &value) { if (OAISerializerSettings::getInstance()->getCustomDateTimeFormat().isStringSet) { return value.toString(OAISerializerSettings::getInstance()->getCustomDateTimeFormat().formatString); } if (OAISerializerSettings::getInstance()->getCustomDateTimeFormat().isEnumSet) { return value.toString(OAISerializerSettings::getInstance()->getCustomDateTimeFormat().formatEnum); } // ISO 8601 return value.toString(Qt::ISODate); } QString toStringValue(const QByteArray &value) { return QString(value); } QString toStringValue(const QDate &value) { // ISO 8601 return value.toString(Qt::DateFormat::ISODate); } QString toStringValue(const qint32 &value) { return QString::number(value); } QString toStringValue(const qint64 &value) { return QString::number(value); } QString toStringValue(const bool &value) { return QString(value ? "true" : "false"); } QString toStringValue(const float &value) { return QString::number(static_cast(value)); } QString toStringValue(const double &value) { return QString::number(value); } QString toStringValue(const OAIObject &value) { return value.asJson(); } QString toStringValue(const OAIEnum &value) { return value.asJson(); } QString toStringValue(const OAIHttpFileElement &value) { return value.asJson(); } QJsonValue toJsonValue(const QString &value) { return QJsonValue(value); } QJsonValue toJsonValue(const QDateTime &value) { if (OAISerializerSettings::getInstance()->getCustomDateTimeFormat().isStringSet) { return QJsonValue(value.toString(OAISerializerSettings::getInstance()->getCustomDateTimeFormat().formatString)); } if (OAISerializerSettings::getInstance()->getCustomDateTimeFormat().isEnumSet) { return QJsonValue(value.toString(OAISerializerSettings::getInstance()->getCustomDateTimeFormat().formatEnum)); } // ISO 8601 return QJsonValue(value.toString(Qt::ISODate)); } QJsonValue toJsonValue(const QByteArray &value) { return QJsonValue(QString(value.toBase64())); } QJsonValue toJsonValue(const QDate &value) { return QJsonValue(value.toString(Qt::ISODate)); } QJsonValue toJsonValue(const qint32 &value) { return QJsonValue(value); } QJsonValue toJsonValue(const qint64 &value) { return QJsonValue(value); } QJsonValue toJsonValue(const bool &value) { return QJsonValue(value); } QJsonValue toJsonValue(const float &value) { return QJsonValue(static_cast(value)); } QJsonValue toJsonValue(const double &value) { return QJsonValue(value); } QJsonValue toJsonValue(const OAIObject &value) { return value.asJsonObject(); } QJsonValue toJsonValue(const OAIEnum &value) { return value.asJsonValue(); } QJsonValue toJsonValue(const OAIHttpFileElement &value) { return value.asJsonValue(); } bool fromStringValue(const QString &inStr, QString &value) { value.clear(); value.append(inStr); return !inStr.isEmpty(); } bool fromStringValue(const QString &inStr, QDateTime &value) { if (inStr.isEmpty()) { return false; } else { QDateTime dateTime; if (OAISerializerSettings::getInstance()->getCustomDateTimeFormat().isStringSet) { dateTime = QDateTime::fromString(inStr, OAISerializerSettings::getInstance()->getCustomDateTimeFormat().formatString); } else if (OAISerializerSettings::getInstance()->getCustomDateTimeFormat().isEnumSet) { dateTime = QDateTime::fromString(inStr, OAISerializerSettings::getInstance()->getCustomDateTimeFormat().formatEnum); } else { dateTime = QDateTime::fromString(inStr, Qt::ISODate); } if (dateTime.isValid()) { value.setDate(dateTime.date()); value.setTime(dateTime.time()); } else { qDebug() << "DateTime is invalid"; } return dateTime.isValid(); } } bool fromStringValue(const QString &inStr, QByteArray &value) { if (inStr.isEmpty()) { return false; } else { value.clear(); value.append(inStr.toUtf8()); return value.count() > 0; } } bool fromStringValue(const QString &inStr, QDate &value) { if (inStr.isEmpty()) { return false; } else { auto date = QDate::fromString(inStr, Qt::DateFormat::ISODate); if (date.isValid()) { value.setDate(date.year(), date.month(), date.day()); } else { qDebug() << "Date is invalid"; } return date.isValid(); } } bool fromStringValue(const QString &inStr, qint32 &value) { bool ok = false; value = QVariant(inStr).toInt(&ok); return ok; } bool fromStringValue(const QString &inStr, qint64 &value) { bool ok = false; value = QVariant(inStr).toLongLong(&ok); return ok; } bool fromStringValue(const QString &inStr, bool &value) { value = QVariant(inStr).toBool(); return ((inStr == "true") || (inStr == "false")); } bool fromStringValue(const QString &inStr, float &value) { bool ok = false; value = QVariant(inStr).toFloat(&ok); return ok; } bool fromStringValue(const QString &inStr, double &value) { bool ok = false; value = QVariant(inStr).toDouble(&ok); return ok; } bool fromStringValue(const QString &inStr, OAIObject &value) { QJsonParseError err; QJsonDocument::fromJson(inStr.toUtf8(),&err); if ( err.error == QJsonParseError::NoError ){ value.fromJson(inStr); return true; } return false; } bool fromStringValue(const QString &inStr, OAIEnum &value) { value.fromJson(inStr); return true; } bool fromStringValue(const QString &inStr, OAIHttpFileElement &value) { return value.fromStringValue(inStr); } bool fromJsonValue(QString &value, const QJsonValue &jval) { bool ok = true; if (!jval.isUndefined() && !jval.isNull()) { if (jval.isString()) { value = jval.toString(); } else if (jval.isBool()) { value = jval.toBool() ? "true" : "false"; } else if (jval.isDouble()) { value = QString::number(jval.toDouble()); } else { ok = false; } } else { ok = false; } return ok; } bool fromJsonValue(QDateTime &value, const QJsonValue &jval) { bool ok = true; if (!jval.isUndefined() && !jval.isNull() && jval.isString()) { if (OAISerializerSettings::getInstance()->getCustomDateTimeFormat().isStringSet) { value = QDateTime::fromString(jval.toString(), OAISerializerSettings::getInstance()->getCustomDateTimeFormat().formatString); } else if (OAISerializerSettings::getInstance()->getCustomDateTimeFormat().isEnumSet) { value = QDateTime::fromString(jval.toString(), OAISerializerSettings::getInstance()->getCustomDateTimeFormat().formatEnum); } else { value = QDateTime::fromString(jval.toString(), Qt::ISODate); } ok = value.isValid(); } else { ok = false; } return ok; } bool fromJsonValue(QByteArray &value, const QJsonValue &jval) { bool ok = true; if (!jval.isUndefined() && !jval.isNull() && jval.isString()) { value = QByteArray::fromBase64(QByteArray::fromStdString(jval.toString().toStdString())); ok = value.size() > 0; } else { ok = false; } return ok; } bool fromJsonValue(QDate &value, const QJsonValue &jval) { bool ok = true; if (!jval.isUndefined() && !jval.isNull() && jval.isString()) { value = QDate::fromString(jval.toString(), Qt::ISODate); ok = value.isValid(); } else { ok = false; } return ok; } bool fromJsonValue(qint32 &value, const QJsonValue &jval) { bool ok = true; if (!jval.isUndefined() && !jval.isNull() && !jval.isObject() && !jval.isArray()) { value = jval.toInt(); } else { ok = false; } return ok; } bool fromJsonValue(qint64 &value, const QJsonValue &jval) { bool ok = true; if (!jval.isUndefined() && !jval.isNull() && !jval.isObject() && !jval.isArray()) { value = jval.toVariant().toLongLong(); } else { ok = false; } return ok; } bool fromJsonValue(bool &value, const QJsonValue &jval) { bool ok = true; if (jval.isBool()) { value = jval.toBool(); } else { ok = false; } return ok; } bool fromJsonValue(float &value, const QJsonValue &jval) { bool ok = true; if (jval.isDouble()) { value = static_cast(jval.toDouble()); } else { ok = false; } return ok; } bool fromJsonValue(double &value, const QJsonValue &jval) { bool ok = true; if (jval.isDouble()) { value = jval.toDouble(); } else { ok = false; } return ok; } bool fromJsonValue(OAIObject &value, const QJsonValue &jval) { bool ok = true; if (jval.isObject()) { value.fromJsonObject(jval.toObject()); ok = value.isValid(); } else { ok = false; } return ok; } bool fromJsonValue(OAIEnum &value, const QJsonValue &jval) { value.fromJsonValue(jval); return true; } bool fromJsonValue(OAIHttpFileElement &value, const QJsonValue &jval) { return value.fromJsonValue(jval); } } // namespace RespExtServer ================================================ FILE: src/modules/extension-server/client/OAIHelpers.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #ifndef OAI_HELPERS_H #define OAI_HELPERS_H #include #include #include #include #include #include #include #include #include #include #include "OAIEnum.h" #include "OAIHttpFileElement.h" #include "OAIObject.h" namespace RespExtServer { template class OptionalParam { public: T m_Value; bool m_hasValue; public: OptionalParam(){ m_hasValue = false; } OptionalParam(const T &val){ m_hasValue = true; m_Value = val; } bool hasValue() const { return m_hasValue; } T value() const{ return m_Value; } }; bool setDateTimeFormat(const QString &format); bool setDateTimeFormat(const Qt::DateFormat &format); template QString toStringValue(const QList &val); template QString toStringValue(const QSet &val); template bool fromStringValue(const QList &inStr, QList &val); template bool fromStringValue(const QSet &inStr, QList &val); template bool fromStringValue(const QMap &inStr, QMap &val); template QJsonValue toJsonValue(const QList &val); template QJsonValue toJsonValue(const QSet &val); template QJsonValue toJsonValue(const QMap &val); template bool fromJsonValue(QList &val, const QJsonValue &jval); template bool fromJsonValue(QSet &val, const QJsonValue &jval); template bool fromJsonValue(QMap &val, const QJsonValue &jval); QString toStringValue(const QString &value); QString toStringValue(const QDateTime &value); QString toStringValue(const QByteArray &value); QString toStringValue(const QDate &value); QString toStringValue(const qint32 &value); QString toStringValue(const qint64 &value); QString toStringValue(const bool &value); QString toStringValue(const float &value); QString toStringValue(const double &value); QString toStringValue(const OAIObject &value); QString toStringValue(const OAIEnum &value); QString toStringValue(const OAIHttpFileElement &value); template QString toStringValue(const QList &val) { QString strArray; for (const auto &item : val) { strArray.append(toStringValue(item) + ","); } if (val.count() > 0) { strArray.chop(1); } return strArray; } template QString toStringValue(const QSet &val) { QString strArray; for (const auto &item : val) { strArray.append(toStringValue(item) + ","); } if (val.count() > 0) { strArray.chop(1); } return strArray; } QJsonValue toJsonValue(const QString &value); QJsonValue toJsonValue(const QDateTime &value); QJsonValue toJsonValue(const QByteArray &value); QJsonValue toJsonValue(const QDate &value); QJsonValue toJsonValue(const qint32 &value); QJsonValue toJsonValue(const qint64 &value); QJsonValue toJsonValue(const bool &value); QJsonValue toJsonValue(const float &value); QJsonValue toJsonValue(const double &value); QJsonValue toJsonValue(const OAIObject &value); QJsonValue toJsonValue(const OAIEnum &value); QJsonValue toJsonValue(const OAIHttpFileElement &value); template QJsonValue toJsonValue(const QList &val) { QJsonArray jArray; for (const auto &item : val) { jArray.append(toJsonValue(item)); } return jArray; } template QJsonValue toJsonValue(const QSet &val) { QJsonArray jArray; for (const auto &item : val) { jArray.append(toJsonValue(item)); } return jArray; } template QJsonValue toJsonValue(const QMap &val) { QJsonObject jObject; for (const auto &itemkey : val.keys()) { jObject.insert(itemkey, toJsonValue(val.value(itemkey))); } return jObject; } bool fromStringValue(const QString &inStr, QString &value); bool fromStringValue(const QString &inStr, QDateTime &value); bool fromStringValue(const QString &inStr, QByteArray &value); bool fromStringValue(const QString &inStr, QDate &value); bool fromStringValue(const QString &inStr, qint32 &value); bool fromStringValue(const QString &inStr, qint64 &value); bool fromStringValue(const QString &inStr, bool &value); bool fromStringValue(const QString &inStr, float &value); bool fromStringValue(const QString &inStr, double &value); bool fromStringValue(const QString &inStr, OAIObject &value); bool fromStringValue(const QString &inStr, OAIEnum &value); bool fromStringValue(const QString &inStr, OAIHttpFileElement &value); template bool fromStringValue(const QList &inStr, QList &val) { bool ok = (inStr.count() > 0); for (const auto &item : inStr) { T itemVal; ok &= fromStringValue(item, itemVal); val.push_back(itemVal); } return ok; } template bool fromStringValue(const QSet &inStr, QList &val) { bool ok = (inStr.count() > 0); for (const auto &item : inStr) { T itemVal; ok &= fromStringValue(item, itemVal); val.push_back(itemVal); } return ok; } template bool fromStringValue(const QMap &inStr, QMap &val) { bool ok = (inStr.count() > 0); for (const auto &itemkey : inStr.keys()) { T itemVal; ok &= fromStringValue(inStr.value(itemkey), itemVal); val.insert(itemkey, itemVal); } return ok; } bool fromJsonValue(QString &value, const QJsonValue &jval); bool fromJsonValue(QDateTime &value, const QJsonValue &jval); bool fromJsonValue(QByteArray &value, const QJsonValue &jval); bool fromJsonValue(QDate &value, const QJsonValue &jval); bool fromJsonValue(qint32 &value, const QJsonValue &jval); bool fromJsonValue(qint64 &value, const QJsonValue &jval); bool fromJsonValue(bool &value, const QJsonValue &jval); bool fromJsonValue(float &value, const QJsonValue &jval); bool fromJsonValue(double &value, const QJsonValue &jval); bool fromJsonValue(OAIObject &value, const QJsonValue &jval); bool fromJsonValue(OAIEnum &value, const QJsonValue &jval); bool fromJsonValue(OAIHttpFileElement &value, const QJsonValue &jval); template bool fromJsonValue(QList &val, const QJsonValue &jval) { bool ok = true; if (jval.isArray()) { for (const auto jitem : jval.toArray()) { T item; ok &= fromJsonValue(item, jitem); val.push_back(item); } } else { ok = false; } return ok; } template bool fromJsonValue(QSet &val, const QJsonValue &jval) { bool ok = true; if (jval.isArray()) { for (const auto jitem : jval.toArray()) { T item; ok &= fromJsonValue(item, jitem); val.insert(item); } } else { ok = false; } return ok; } template bool fromJsonValue(QMap &val, const QJsonValue &jval) { bool ok = true; if (jval.isObject()) { auto varmap = jval.toObject().toVariantMap(); if (varmap.count() > 0) { for (const auto &itemkey : varmap.keys()) { T itemVal; ok &= fromJsonValue(itemVal, QJsonValue::fromVariant(varmap.value(itemkey))); val.insert(itemkey, itemVal); } } } else { ok = false; } return ok; } } // namespace RespExtServer #endif // OAI_HELPERS_H ================================================ FILE: src/modules/extension-server/client/OAIHttpFileElement.cpp ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #include #include #include #include #include "OAIHttpFileElement.h" namespace RespExtServer { void OAIHttpFileElement::setMimeType(const QString &mime) { mime_type = mime; } void OAIHttpFileElement::setFileName(const QString &name) { local_filename = name; } void OAIHttpFileElement::setVariableName(const QString &name) { variable_name = name; } void OAIHttpFileElement::setRequestFileName(const QString &name) { request_filename = name; } bool OAIHttpFileElement::isSet() const { return !local_filename.isEmpty() || !request_filename.isEmpty(); } QString OAIHttpFileElement::asJson() const { QFile file(local_filename); QByteArray bArray; bool result = false; if (file.exists()) { result = file.open(QIODevice::ReadOnly); bArray = file.readAll(); file.close(); } if (!result) { qDebug() << "Error opening file " << local_filename; } return QString(bArray); } QJsonValue OAIHttpFileElement::asJsonValue() const { QFile file(local_filename); QByteArray bArray; bool result = false; if (file.exists()) { result = file.open(QIODevice::ReadOnly); bArray = file.readAll(); file.close(); } if (!result) { qDebug() << "Error opening file " << local_filename; } #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) return QJsonDocument::fromJson(bArray.data()).object(); #else return QJsonDocument::fromBinaryData(bArray.data()).object(); #endif } bool OAIHttpFileElement::fromStringValue(const QString &instr) { QFile file(local_filename); bool result = false; if (file.exists()) { file.remove(); } result = file.open(QIODevice::WriteOnly); file.write(instr.toUtf8()); file.close(); if (!result) { qDebug() << "Error creating file " << local_filename; } return result; } bool OAIHttpFileElement::fromJsonValue(const QJsonValue &jval) { QFile file(local_filename); bool result = false; if (file.exists()) { file.remove(); } result = file.open(QIODevice::WriteOnly); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) file.write(QJsonDocument(jval.toObject()).toJson()); #else file.write(QJsonDocument(jval.toObject()).toBinaryData()); #endif file.close(); if (!result) { qDebug() << "Error creating file " << local_filename; } return result; } QByteArray OAIHttpFileElement::asByteArray() const { QFile file(local_filename); QByteArray bArray; bool result = false; if (file.exists()) { result = file.open(QIODevice::ReadOnly); bArray = file.readAll(); file.close(); } if (!result) { qDebug() << "Error opening file " << local_filename; } return bArray; } bool OAIHttpFileElement::fromByteArray(const QByteArray &bytes) { QFile file(local_filename); bool result = false; if (file.exists()) { file.remove(); } result = file.open(QIODevice::WriteOnly); file.write(bytes); file.close(); if (!result) { qDebug() << "Error creating file " << local_filename; } return result; } bool OAIHttpFileElement::saveToFile(const QString &varName, const QString &localFName, const QString &reqFname, const QString &mime, const QByteArray &bytes) { setMimeType(mime); setFileName(localFName); setVariableName(varName); setRequestFileName(reqFname); return fromByteArray(bytes); } QByteArray OAIHttpFileElement::loadFromFile(const QString &varName, const QString &localFName, const QString &reqFname, const QString &mime) { setMimeType(mime); setFileName(localFName); setVariableName(varName); setRequestFileName(reqFname); return asByteArray(); } } // namespace RespExtServer ================================================ FILE: src/modules/extension-server/client/OAIHttpFileElement.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #ifndef OAI_HTTP_FILE_ELEMENT_H #define OAI_HTTP_FILE_ELEMENT_H #include #include #include namespace RespExtServer { class OAIHttpFileElement { public: QString variable_name; QString local_filename; QString request_filename; QString mime_type; void setMimeType(const QString &mime); void setFileName(const QString &name); void setVariableName(const QString &name); void setRequestFileName(const QString &name); bool isSet() const; bool fromStringValue(const QString &instr); bool fromJsonValue(const QJsonValue &jval); bool fromByteArray(const QByteArray &bytes); bool saveToFile(const QString &variable_name, const QString &local_filename, const QString &request_filename, const QString &mime, const QByteArray &bytes); QString asJson() const; QJsonValue asJsonValue() const; QByteArray asByteArray() const; QByteArray loadFromFile(const QString &variable_name, const QString &local_filename, const QString &request_filename, const QString &mime); }; } // namespace RespExtServer Q_DECLARE_METATYPE(RespExtServer::OAIHttpFileElement) #endif // OAI_HTTP_FILE_ELEMENT_H ================================================ FILE: src/modules/extension-server/client/OAIHttpRequest.cpp ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #include #include #include #include #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #define SKIP_EMPTY_PARTS Qt::SkipEmptyParts #else #define SKIP_EMPTY_PARTS QString::SkipEmptyParts #endif #include "OAIHttpRequest.h" namespace RespExtServer { OAIHttpRequestInput::OAIHttpRequestInput() { initialize(); } OAIHttpRequestInput::OAIHttpRequestInput(QString v_url_str, QString v_http_method) { initialize(); url_str = v_url_str; http_method = v_http_method; } void OAIHttpRequestInput::initialize() { var_layout = NOT_SET; url_str = ""; http_method = "GET"; } void OAIHttpRequestInput::add_var(QString key, QString value) { vars[key] = value; } void OAIHttpRequestInput::add_file(QString variable_name, QString local_filename, QString request_filename, QString mime_type) { OAIHttpFileElement file; file.variable_name = variable_name; file.local_filename = local_filename; file.request_filename = request_filename; file.mime_type = mime_type; files.append(file); } OAIHttpRequestWorker::OAIHttpRequestWorker(QObject *parent, QNetworkAccessManager *_manager) : QObject(parent), manager(_manager), timeOutTimer(this), isResponseCompressionEnabled(false), isRequestCompressionEnabled(false), httpResponseCode(-1) { #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) randomGenerator = QRandomGenerator(QDateTime::currentDateTime().toSecsSinceEpoch()); #else qsrand(QDateTime::currentDateTime().toTime_t()); #endif if (manager == nullptr) { manager = new QNetworkAccessManager(this); } workingDirectory = QDir::currentPath(); timeOutTimer.setSingleShot(true); } OAIHttpRequestWorker::~OAIHttpRequestWorker() { QObject::disconnect(&timeOutTimer, &QTimer::timeout, nullptr, nullptr); timeOutTimer.stop(); for (const auto &item : multiPartFields) { if (item != nullptr) { delete item; } } } QMap OAIHttpRequestWorker::getResponseHeaders() const { return headers; } OAIHttpFileElement OAIHttpRequestWorker::getHttpFileElement(const QString &fieldname) { if (!files.isEmpty()) { if (fieldname.isEmpty()) { return files.first(); } else if (files.contains(fieldname)) { return files[fieldname]; } } return OAIHttpFileElement(); } QByteArray *OAIHttpRequestWorker::getMultiPartField(const QString &fieldname) { if (!multiPartFields.isEmpty()) { if (fieldname.isEmpty()) { return multiPartFields.first(); } else if (multiPartFields.contains(fieldname)) { return multiPartFields[fieldname]; } } return nullptr; } void OAIHttpRequestWorker::setTimeOut(int timeOutMs) { timeOutTimer.setInterval(timeOutMs); if(timeOutTimer.interval() == 0) { QObject::disconnect(&timeOutTimer, &QTimer::timeout, nullptr, nullptr); } } void OAIHttpRequestWorker::setWorkingDirectory(const QString &path) { if (!path.isEmpty()) { workingDirectory = path; } } void OAIHttpRequestWorker::setResponseCompressionEnabled(bool enable) { isResponseCompressionEnabled = enable; } void OAIHttpRequestWorker::setRequestCompressionEnabled(bool enable) { isRequestCompressionEnabled = enable; } int OAIHttpRequestWorker::getHttpResponseCode() const{ return httpResponseCode; } QString OAIHttpRequestWorker::http_attribute_encode(QString attribute_name, QString input) { // result structure follows RFC 5987 bool need_utf_encoding = false; QString result = ""; QByteArray input_c = input.toLocal8Bit(); char c; for (int i = 0; i < input_c.length(); i++) { c = input_c.at(i); if (c == '\\' || c == '/' || c == '\0' || c < ' ' || c > '~') { // ignore and request utf-8 version need_utf_encoding = true; } else if (c == '"') { result += "\\\""; } else { result += c; } } if (result.length() == 0) { need_utf_encoding = true; } if (!need_utf_encoding) { // return simple version return QString("%1=\"%2\"").arg(attribute_name, result); } QString result_utf8 = ""; for (int i = 0; i < input_c.length(); i++) { c = input_c.at(i); if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { result_utf8 += c; } else { result_utf8 += "%" + QString::number(static_cast(input_c.at(i)), 16).toUpper(); } } // return enhanced version with UTF-8 support return QString("%1=\"%2\"; %1*=utf-8''%3").arg(attribute_name, result, result_utf8); } void OAIHttpRequestWorker::execute(OAIHttpRequestInput *input) { // reset variables QNetworkReply *reply = nullptr; QByteArray request_content = ""; response = ""; error_type = QNetworkReply::NoError; error_str = ""; bool isFormData = false; // decide on the variable layout if (input->files.length() > 0) { input->var_layout = MULTIPART; } if (input->var_layout == NOT_SET) { input->var_layout = input->http_method == "GET" || input->http_method == "HEAD" ? ADDRESS : URL_ENCODED; } // prepare request content QString boundary = ""; if (input->var_layout == ADDRESS || input->var_layout == URL_ENCODED) { // variable layout is ADDRESS or URL_ENCODED if (input->vars.count() > 0) { bool first = true; isFormData = true; foreach (QString key, input->vars.keys()) { if (!first) { request_content.append("&"); } first = false; request_content.append(QUrl::toPercentEncoding(key)); request_content.append("="); request_content.append(QUrl::toPercentEncoding(input->vars.value(key))); } if (input->var_layout == ADDRESS) { input->url_str += "?" + request_content; request_content = ""; } } } else { // variable layout is MULTIPART boundary = QString("__-----------------------%1%2") #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) .arg(QDateTime::currentDateTime().toSecsSinceEpoch()) .arg(randomGenerator.generate()); #else .arg(QDateTime::currentDateTime().toTime_t()) .arg(qrand()); #endif QString boundary_delimiter = "--"; QString new_line = "\r\n"; // add variables foreach (QString key, input->vars.keys()) { // add boundary request_content.append(boundary_delimiter.toUtf8()); request_content.append(boundary.toUtf8()); request_content.append(new_line.toUtf8()); // add header request_content.append("Content-Disposition: form-data; "); request_content.append(http_attribute_encode("name", key).toUtf8()); request_content.append(new_line.toUtf8()); request_content.append("Content-Type: text/plain"); request_content.append(new_line.toUtf8()); // add header to body splitter request_content.append(new_line.toUtf8()); // add variable content request_content.append(input->vars.value(key).toUtf8()); request_content.append(new_line.toUtf8()); } // add files for (QList::iterator file_info = input->files.begin(); file_info != input->files.end(); file_info++) { QFileInfo fi(file_info->local_filename); // ensure necessary variables are available if (file_info->local_filename == nullptr || file_info->local_filename.isEmpty() || file_info->variable_name == nullptr || file_info->variable_name.isEmpty() || !fi.exists() || !fi.isFile() || !fi.isReadable()) { // silent abort for the current file continue; } QFile file(file_info->local_filename); if (!file.open(QIODevice::ReadOnly)) { // silent abort for the current file continue; } // ensure filename for the request if (file_info->request_filename == nullptr || file_info->request_filename.isEmpty()) { file_info->request_filename = fi.fileName(); if (file_info->request_filename.isEmpty()) { file_info->request_filename = "file"; } } // add boundary request_content.append(boundary_delimiter.toUtf8()); request_content.append(boundary.toUtf8()); request_content.append(new_line.toUtf8()); // add header request_content.append( QString("Content-Disposition: form-data; %1; %2").arg(http_attribute_encode("name", file_info->variable_name), http_attribute_encode("filename", file_info->request_filename)).toUtf8()); request_content.append(new_line.toUtf8()); if (file_info->mime_type != nullptr && !file_info->mime_type.isEmpty()) { request_content.append("Content-Type: "); request_content.append(file_info->mime_type.toUtf8()); request_content.append(new_line.toUtf8()); } request_content.append("Content-Transfer-Encoding: binary"); request_content.append(new_line.toUtf8()); // add header to body splitter request_content.append(new_line.toUtf8()); // add file content request_content.append(file.readAll()); request_content.append(new_line.toUtf8()); file.close(); } // add end of body request_content.append(boundary_delimiter.toUtf8()); request_content.append(boundary.toUtf8()); request_content.append(boundary_delimiter.toUtf8()); } if (input->request_body.size() > 0) { qDebug() << "got a request body"; request_content.clear(); if(!isFormData && (input->var_layout != MULTIPART) && isRequestCompressionEnabled){ request_content.append(compress(input->request_body, 7, OAICompressionType::Gzip)); } else { request_content.append(input->request_body); } } // prepare connection QNetworkRequest request = QNetworkRequest(QUrl(input->url_str)); if (OAIHttpRequestWorker::sslDefaultConfiguration != nullptr) { request.setSslConfiguration(*OAIHttpRequestWorker::sslDefaultConfiguration); } request.setRawHeader("User-Agent", "OpenAPI-Generator/1.0.0/cpp-qt"); foreach (QString key, input->headers.keys()) { request.setRawHeader(key.toStdString().c_str(), input->headers.value(key).toStdString().c_str()); } if (request_content.size() > 0 && !isFormData && (input->var_layout != MULTIPART)) { if (!input->headers.contains("Content-Type")) { request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); } else { request.setHeader(QNetworkRequest::ContentTypeHeader, input->headers.value("Content-Type")); } if(isRequestCompressionEnabled){ request.setRawHeader("Content-Encoding", "gzip"); } } else if (input->var_layout == URL_ENCODED) { request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); } else if (input->var_layout == MULTIPART) { request.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data; boundary=" + boundary); } if(isResponseCompressionEnabled){ request.setRawHeader("Accept-Encoding", "gzip"); } else { request.setRawHeader("Accept-Encoding", "identity"); } if (input->http_method == "GET") { reply = manager->get(request); } else if (input->http_method == "POST") { reply = manager->post(request, request_content); } else if (input->http_method == "PUT") { reply = manager->put(request, request_content); } else if (input->http_method == "HEAD") { reply = manager->head(request); } else if (input->http_method == "DELETE") { reply = manager->deleteResource(request); } else { #if (QT_VERSION >= 0x050800) reply = manager->sendCustomRequest(request, input->http_method.toLatin1(), request_content); #else QBuffer *buffer = new QBuffer; buffer->setData(request_content); buffer->open(QIODevice::ReadOnly); reply = manager->sendCustomRequest(request, input->http_method.toLatin1(), buffer); buffer->setParent(reply); #endif } if (reply != nullptr) { reply->setParent(this); connect(reply, &QNetworkReply::finished, [this, reply] { on_reply_finished(reply); }); } if (timeOutTimer.interval() > 0) { QObject::connect(&timeOutTimer, &QTimer::timeout, [this, reply] { on_reply_timeout(reply); }); timeOutTimer.start(); } } void OAIHttpRequestWorker::on_reply_finished(QNetworkReply *reply) { bool codeSts = false; if(timeOutTimer.isActive()) { QObject::disconnect(&timeOutTimer, &QTimer::timeout, nullptr, nullptr); timeOutTimer.stop(); } error_type = reply->error(); error_str = reply->errorString(); if (reply->rawHeaderPairs().count() > 0) { for (const auto &item : reply->rawHeaderPairs()) { headers.insert(item.first, item.second); } } auto rescode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(&codeSts); if(codeSts){ httpResponseCode = rescode; } else{ httpResponseCode = -1; } process_response(reply); reply->deleteLater(); emit on_execution_finished(this); } void OAIHttpRequestWorker::on_reply_timeout(QNetworkReply *reply) { error_type = QNetworkReply::TimeoutError; response = ""; error_str = "Timed out waiting for response"; disconnect(reply, nullptr, nullptr, nullptr); reply->abort(); reply->deleteLater(); emit on_execution_finished(this); } void OAIHttpRequestWorker::process_response(QNetworkReply *reply) { QString contentDispositionHdr; QString contentTypeHdr; QString contentEncodingHdr; for(auto hdr: getResponseHeaders().keys()){ if(hdr.compare(QString("Content-Disposition"), Qt::CaseInsensitive) == 0){ contentDispositionHdr = getResponseHeaders().value(hdr); } if(hdr.compare(QString("Content-Type"), Qt::CaseInsensitive) == 0){ contentTypeHdr = getResponseHeaders().value(hdr); } if(hdr.compare(QString("Content-Encoding"), Qt::CaseInsensitive) == 0){ contentEncodingHdr = getResponseHeaders().value(hdr); } } if (!contentDispositionHdr.isEmpty()) { auto contentDisposition = contentDispositionHdr.split(QString(";"), SKIP_EMPTY_PARTS); auto contentType = !contentTypeHdr.isEmpty() ? contentTypeHdr.split(QString(";"), SKIP_EMPTY_PARTS).first() : QString(); if ((contentDisposition.count() > 0) && (contentDisposition.first() == QString("attachment"))) { QString filename = QUuid::createUuid().toString(); for (const auto &file : contentDisposition) { if (file.contains(QString("filename"))) { filename = file.split(QString("="), SKIP_EMPTY_PARTS).at(1); break; } } OAIHttpFileElement felement; felement.saveToFile(QString(), workingDirectory + QDir::separator() + filename, filename, contentType, reply->readAll()); files.insert(filename, felement); } } else if (!contentTypeHdr.isEmpty()) { auto contentType = contentTypeHdr.split(QString(";"), SKIP_EMPTY_PARTS); if ((contentType.count() > 0) && (contentType.first() == QString("multipart/form-data"))) { // TODO : Handle Multipart responses } else { if(!contentEncodingHdr.isEmpty()){ auto encoding = contentEncodingHdr.split(QString(";"), SKIP_EMPTY_PARTS); if(encoding.count() > 0){ auto compressionTypes = encoding.first().split(',', SKIP_EMPTY_PARTS); if(compressionTypes.contains("gzip", Qt::CaseInsensitive) || compressionTypes.contains("deflate", Qt::CaseInsensitive)){ response = decompress(reply->readAll()); } else if(compressionTypes.contains("identity", Qt::CaseInsensitive)){ response = reply->readAll(); } } } else { response = reply->readAll(); } } } } QByteArray OAIHttpRequestWorker::decompress(const QByteArray& data){ Q_UNUSED(data); return QByteArray(); } QByteArray OAIHttpRequestWorker::compress(const QByteArray& input, int level, OAICompressionType compressType) { Q_UNUSED(input); Q_UNUSED(level); Q_UNUSED(compressType); return QByteArray(); } QSslConfiguration *OAIHttpRequestWorker::sslDefaultConfiguration; } // namespace RespExtServer ================================================ FILE: src/modules/extension-server/client/OAIHttpRequest.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * Based on http://www.creativepulse.gr/en/blog/2014/restful-api-requests-using-qt-cpp-for-linux-mac-osx-ms-windows * By Alex Stylianos * **/ #ifndef OAI_HTTPREQUESTWORKER_H #define OAI_HTTPREQUESTWORKER_H #include #include #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #include #endif #include "OAIHttpFileElement.h" namespace RespExtServer { enum OAIHttpRequestVarLayout { NOT_SET, ADDRESS, URL_ENCODED, MULTIPART }; class OAIHttpRequestInput { public: QString url_str; QString http_method; OAIHttpRequestVarLayout var_layout; QMap vars; QMap headers; QList files; QByteArray request_body; OAIHttpRequestInput(); OAIHttpRequestInput(QString v_url_str, QString v_http_method); void initialize(); void add_var(QString key, QString value); void add_file(QString variable_name, QString local_filename, QString request_filename, QString mime_type); }; class OAIHttpRequestWorker : public QObject { Q_OBJECT public: explicit OAIHttpRequestWorker(QObject *parent = nullptr, QNetworkAccessManager *manager = nullptr); virtual ~OAIHttpRequestWorker(); QByteArray response; QNetworkReply::NetworkError error_type; QString error_str; QMap getResponseHeaders() const; QString http_attribute_encode(QString attribute_name, QString input); void execute(OAIHttpRequestInput *input); static QSslConfiguration *sslDefaultConfiguration; void setTimeOut(int timeOutMs); void setWorkingDirectory(const QString &path); OAIHttpFileElement getHttpFileElement(const QString &fieldname = QString()); QByteArray *getMultiPartField(const QString &fieldname = QString()); void setResponseCompressionEnabled(bool enable); void setRequestCompressionEnabled(bool enable); int getHttpResponseCode() const; signals: void on_execution_finished(OAIHttpRequestWorker *worker); private: enum OAICompressionType{ Zlib, Gzip }; QNetworkAccessManager *manager; QMap headers; QMap files; QMap multiPartFields; QString workingDirectory; QTimer timeOutTimer; bool isResponseCompressionEnabled; bool isRequestCompressionEnabled; int httpResponseCode; #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) QRandomGenerator randomGenerator; #endif void on_reply_timeout(QNetworkReply *reply); void on_reply_finished(QNetworkReply *reply); void process_response(QNetworkReply *reply); QByteArray decompress(const QByteArray& data); QByteArray compress(const QByteArray& input, int level, OAICompressionType compressType); }; } // namespace RespExtServer #endif // OAI_HTTPREQUESTWORKER_H ================================================ FILE: src/modules/extension-server/client/OAIInline_response_400.cpp ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #include "OAIInline_response_400.h" #include #include #include #include #include "OAIHelpers.h" namespace RespExtServer { OAIInline_response_400::OAIInline_response_400(QString json) { this->initializeModel(); this->fromJson(json); } OAIInline_response_400::OAIInline_response_400() { this->initializeModel(); } OAIInline_response_400::~OAIInline_response_400() {} void OAIInline_response_400::initializeModel() { m_error_isSet = false; m_error_isValid = false; } void OAIInline_response_400::fromJson(QString jsonString) { QByteArray array(jsonString.toStdString().c_str()); QJsonDocument doc = QJsonDocument::fromJson(array); QJsonObject jsonObject = doc.object(); this->fromJsonObject(jsonObject); } void OAIInline_response_400::fromJsonObject(QJsonObject json) { m_error_isValid = ::RespExtServer::fromJsonValue(error, json[QString("error")]); m_error_isSet = !json[QString("error")].isNull() && m_error_isValid; } QString OAIInline_response_400::asJson() const { QJsonObject obj = this->asJsonObject(); QJsonDocument doc(obj); QByteArray bytes = doc.toJson(); return QString(bytes); } QJsonObject OAIInline_response_400::asJsonObject() const { QJsonObject obj; if (m_error_isSet) { obj.insert(QString("error"), ::RespExtServer::toJsonValue(error)); } return obj; } QString OAIInline_response_400::getError() const { return error; } void OAIInline_response_400::setError(const QString &error) { this->error = error; this->m_error_isSet = true; } bool OAIInline_response_400::is_error_Set() const{ return m_error_isSet; } bool OAIInline_response_400::is_error_Valid() const{ return m_error_isValid; } bool OAIInline_response_400::isSet() const { bool isObjectUpdated = false; do { if (m_error_isSet) { isObjectUpdated = true; break; } } while (false); return isObjectUpdated; } bool OAIInline_response_400::isValid() const { // only required properties are required for the object to be considered valid return true; } } // namespace RespExtServer ================================================ FILE: src/modules/extension-server/client/OAIInline_response_400.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /* * OAIInline_response_400.h * * */ #ifndef OAIInline_response_400_H #define OAIInline_response_400_H #include #include #include "OAIEnum.h" #include "OAIObject.h" namespace RespExtServer { class OAIInline_response_400 : public OAIObject { public: OAIInline_response_400(); OAIInline_response_400(QString json); ~OAIInline_response_400() override; QString asJson() const override; QJsonObject asJsonObject() const override; void fromJsonObject(QJsonObject json) override; void fromJson(QString jsonString) override; QString getError() const; void setError(const QString &error); bool is_error_Set() const; bool is_error_Valid() const; virtual bool isSet() const override; virtual bool isValid() const override; private: void initializeModel(); QString error; bool m_error_isSet; bool m_error_isValid; }; } // namespace RespExtServer Q_DECLARE_METATYPE(RespExtServer::OAIInline_response_400) #endif // OAIInline_response_400_H ================================================ FILE: src/modules/extension-server/client/OAIOauth.cpp ================================================ #include "OAIOauth.h" namespace RespExtServer { /* * Base class to perform oauth2 flows * */ void OauthBase::onFinish(QNetworkReply *rep) { //TODO emit error signal when token is wrong QJsonDocument document = QJsonDocument::fromJson(rep->readAll()); QJsonObject rootObj = document.object(); QString token = rootObj.find("access_token").value().toString(); QString scope = rootObj.find("scope").value().toString(); QString type = rootObj.find("token_type").value().toString(); int expiresIn = rootObj.find("expires_in").value().toInt(); addToken(oauthToken(token, expiresIn, scope, type)); } oauthToken OauthBase::getToken(QString scope) { auto tokenIt = m_oauthTokenMap.find(scope); return tokenIt != m_oauthTokenMap.end() ? tokenIt.value() : oauthToken(); } void OauthBase::addToken(oauthToken token) { m_oauthTokenMap.insert(token.getScope(),token); emit tokenReceived(); } void OauthBase::removeToken(QString scope) { m_oauthTokenMap.remove(scope); } /* * Class to perform the authorization code flow * */ OauthCode::OauthCode(QObject *parent) : OauthBase(parent){} void OauthCode::link(){ connect(&m_server, SIGNAL(dataReceived(QMap)), this, SLOT(onVerificationReceived(QMap))); connect(this, SIGNAL(authenticationNeeded()), this, SLOT(authenticationNeededCallback())); connect(this, SIGNAL(tokenReceived()), &m_server, SLOT(stop())); } void OauthCode::unlink() { disconnect(this,0,0,0); disconnect(&m_server,0,0,0); } void OauthCode::setVariables(QString authUrl, QString tokenUrl, QString scope, QString state, QString redirectUri, QString clientId, QString clientSecret, QString accessType){ m_authUrl = QUrl(authUrl); m_tokenUrl = QUrl(tokenUrl); m_scope = scope; m_accessType = accessType; m_state = state; m_redirectUri = redirectUri; m_clientId = clientId; m_clientSecret = clientSecret; } void OauthCode::authenticationNeededCallback() { QDesktopServices::openUrl(QUrl(m_authUrl.toString() + "?scope=" + m_scope + (m_accessType=="" ? "" : "&access_type=" + m_accessType) + "&response_type=code" + "&state=" + m_state + "&redirect_uri=" + m_redirectUri + "&client_id=" + m_clientId)); m_server.start(); } void OauthCode::onVerificationReceived(const QMap response) { // Save access code QString state(response.value("state")); QString scope(response.value("scope")); QString code(response.value("code")); //create query with the required fields QUrlQuery postData; postData.addQueryItem("grant_type", "authorization_code"); postData.addQueryItem("client_id", m_clientId); postData.addQueryItem("client_secret", m_clientSecret); postData.addQueryItem("code", code); postData.addQueryItem("redirect_uri", m_redirectUri); QNetworkAccessManager * manager = new QNetworkAccessManager(this); QNetworkRequest request(m_tokenUrl); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(onFinish(QNetworkReply *))); manager->post(request, postData.query().toUtf8()); } /* * Class to perform the implicit flow * */ OauthImplicit::OauthImplicit(QObject *parent) : OauthBase(parent){} void OauthImplicit::link() { //TODO correct linking connect(&m_server, SIGNAL(dataReceived(QMap)), this, SLOT(ImplicitTokenReceived(QMap))); connect(this, SIGNAL(authenticationNeeded()), this, SLOT(authenticationNeededCallback())); connect(this, SIGNAL(tokenReceived()), &m_server, SLOT(stop())); m_linked = true; } void OauthImplicit::unlink() { disconnect(this,0,0,0); disconnect(&m_server,0,0,0); m_linked = false; } void OauthImplicit::setVariables(QString authUrl, QString scope, QString state, QString redirectUri, QString clientId, QString accessType){ m_authUrl = QUrl(authUrl); m_scope = scope; m_accessType = accessType; m_state = state; m_redirectUri = redirectUri; m_clientId = clientId; } void OauthImplicit::authenticationNeededCallback() { QDesktopServices::openUrl(QUrl(m_authUrl.toString() + "?scope=" + m_scope + (m_accessType=="" ? "" : "&access_type=" + m_accessType) + "&response_type=token" + "&state=" + m_state + "&redirect_uri=" + m_redirectUri + "&client_id=" + m_clientId)); m_server.start(); } void OauthImplicit::ImplicitTokenReceived(const QMap response) { QString token = response.find("access_token").value(); QString scope = response.find("scope").value(); QString type = response.find("token_type").value(); int expiresIn = response.find("expires_in").value().toInt(); addToken(oauthToken(token, expiresIn, scope, type)); } /* * Class to perform the client credentials flow * */ OauthCredentials::OauthCredentials(QObject *parent) : OauthBase(parent){} void OauthCredentials::link() { connect(this, SIGNAL(authenticationNeeded()), this, SLOT(authenticationNeededCallback())); } void OauthCredentials::unlink() { disconnect(this,0,0,0); } void OauthCredentials::setVariables(QString tokenUrl, QString scope, QString clientId, QString clientSecret){ m_tokenUrl = QUrl(tokenUrl); m_scope = scope; m_clientId = clientId; m_clientSecret = clientSecret; } void OauthCredentials::authenticationNeededCallback() { //create query with the required fields QUrlQuery postData; postData.addQueryItem("grant_type", "client_credentials"); postData.addQueryItem("client_id", m_clientId); postData.addQueryItem("client_secret", m_clientSecret); postData.addQueryItem("scope", m_scope); QNetworkAccessManager * manager = new QNetworkAccessManager(this); QNetworkRequest request(m_tokenUrl); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(onFinish(QNetworkReply *))); manager->post(request, postData.query().toUtf8()); } /* * Class to perform the resource owner password flow * */ OauthPassword::OauthPassword(QObject *parent) : OauthBase(parent){} void OauthPassword::link() { connect(this, SIGNAL(authenticationNeeded()), this, SLOT(authenticationNeededCallback())); } void OauthPassword::unlink() { disconnect(this,0,0,0); } void OauthPassword::setVariables(QString tokenUrl, QString scope, QString clientId, QString clientSecret, QString username, QString password){ m_tokenUrl = QUrl(tokenUrl); m_scope = scope; m_clientId = clientId; m_clientSecret = clientSecret; m_username = username; m_password = password; } void OauthPassword::authenticationNeededCallback() { //create query with the required fields QUrlQuery postData; postData.addQueryItem("grant_type", "password"); postData.addQueryItem("username", m_username); postData.addQueryItem("password", m_password); postData.addQueryItem("client_id", m_clientId); postData.addQueryItem("client_secret", m_clientSecret); postData.addQueryItem("scope", m_scope); QNetworkAccessManager * manager = new QNetworkAccessManager(this); QNetworkRequest request(m_tokenUrl); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(onFinish(QNetworkReply *))); manager->post(request, postData.query().toUtf8()); } /* * Class that provides a simple reply server * */ ReplyServer::ReplyServer(QObject *parent) : QTcpServer(parent) { connect(this, SIGNAL(newConnection()), this, SLOT(onConnected())); m_reply ="you can close this window now!"; } void ReplyServer::start() { if(!listen(QHostAddress::Any, 9999)) { qDebug() << "Server could not start"; } else { qDebug() << "Server started!"; } } void ReplyServer::stop() { qDebug() << "Stopping the Server..."; QTcpServer::close(); } void ReplyServer::onConnected() { // need to grab the socket QTcpSocket *socket = nextPendingConnection(); connect(socket, SIGNAL(readyRead()), this, SLOT(read()), Qt::UniqueConnection); connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater())); } void ReplyServer::read() { QTcpSocket *socket = qobject_cast(sender()); if (!socket) { qDebug() << "No socket available"; return; } qDebug() << "Socket connected"; QTextStream os(socket); os.setAutoDetectUnicode(true); os << "HTTP/1.0 200 Ok\r\n" "Content-Type: text/html; charset=\"utf-8\"\r\n" "\r\n" <<"\ \ \ \ \ \

You can close this window now!

\ \ "; QByteArray data = socket->readLine(); QString splitGetLine = QString(data); splitGetLine.remove("GET"); splitGetLine.remove("HTTP/1.1"); splitGetLine.remove("\r\n"); splitGetLine.remove(" "); //prefix is needed to extract query params QUrl getTokenUrl("http://" + splitGetLine); QList< QPair > tokens; QUrlQuery query(getTokenUrl); tokens = query.queryItems(); QMap queryParams; QPair tokenPair; foreach (tokenPair, tokens) { QString key = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.first.trimmed().toLatin1())); QString value = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.second.trimmed().toLatin1())); queryParams.insert(key, value); } if (!queryParams.contains("state")) { socket->close(); return; } socket->close(); emit dataReceived(queryParams); } } // namespace RespExtServer ================================================ FILE: src/modules/extension-server/client/OAIOauth.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * Providing a Oauth2 Class and a ReplyServer for the Oauth2 authorization code flow. */ #ifndef OAI_OAUTH2_H #define OAI_OAUTH2_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace RespExtServer { class oauthToken { public: oauthToken(QString token, int expiresIn, QString scope, QString tokenType) : m_token(token), m_scope(scope), m_type(tokenType){ m_validUntil = time(0) + expiresIn; } oauthToken(){ m_validUntil = time(0) - 1; } QString getToken(){return m_token;}; QString getScope(){return m_scope;}; QString getType(){return m_type;}; bool isValid(){return time(0) < m_validUntil;}; private: QString m_token; time_t m_validUntil; QString m_scope; QString m_type; }; class ReplyServer : public QTcpServer { Q_OBJECT public: explicit ReplyServer(QObject *parent = nullptr); void setReply(QByteArray reply){m_reply = reply;}; void run(); private: QByteArray m_reply; signals: void dataReceived(QMap); public slots: void onConnected(); void read(); void start(); void stop(); }; //Base class class OauthBase : public QObject { Q_OBJECT public: OauthBase(QObject* parent = nullptr) : QObject(parent) {}; oauthToken getToken(QString scope); void addToken(oauthToken token); void removeToken(QString scope); bool linked(){return m_linked;}; virtual void link()=0; virtual void unlink()=0; protected: QMap m_oauthTokenMap; QUrl m_authUrl; QUrl m_tokenUrl; QString m_scope, m_accessType, m_state, m_redirectUri, m_clientId, m_clientSecret; bool m_linked; public slots: virtual void authenticationNeededCallback()=0; void onFinish(QNetworkReply *rep); signals: void authenticationNeeded(); void tokenReceived(); }; // Authorization code flow class OauthCode : public OauthBase { Q_OBJECT public: OauthCode(QObject *parent = nullptr); void link() override; void unlink() override; void setVariables(QString authUrl, QString tokenUrl, QString scope, QString state, QString redirectUri, QString clientId, QString clientSecret, QString accessType = ""); private: ReplyServer m_server; public slots: void authenticationNeededCallback() override; void onVerificationReceived(const QMap response); }; // Implicit flow class OauthImplicit : public OauthBase { Q_OBJECT public: OauthImplicit(QObject *parent = nullptr); void link() override; void unlink() override; void setVariables(QString authUrl, QString scope, QString state, QString redirectUri, QString clientId, QString accessType = ""); private: ReplyServer m_server; public slots: void authenticationNeededCallback() override; void ImplicitTokenReceived(const QMap response); }; //client credentials flow class OauthCredentials : public OauthBase { Q_OBJECT public: OauthCredentials(QObject *parent = nullptr); void link() override; void unlink() override; void setVariables(QString tokenUrl, QString scope, QString clientId, QString clientSecret); public slots: void authenticationNeededCallback() override; }; //resource owner password flow class OauthPassword : public OauthBase { Q_OBJECT public: OauthPassword(QObject *parent = nullptr); void link() override; void unlink() override; void setVariables(QString tokenUrl, QString scope, QString clientId, QString clientSecret, QString username, QString password); private: QString m_username, m_password; public slots: void authenticationNeededCallback() override; }; } // namespace RespExtServer #endif // OAI_OAUTH2_H ================================================ FILE: src/modules/extension-server/client/OAIObject.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ #ifndef OAI_OBJECT_H #define OAI_OBJECT_H #include #include #include namespace RespExtServer { class OAIObject { public: OAIObject() {} OAIObject(QString jsonString) { fromJson(jsonString); } virtual ~OAIObject() {} virtual QJsonObject asJsonObject() const { return jObj; } virtual QString asJson() const { QJsonDocument doc(jObj); return doc.toJson(QJsonDocument::Compact); } virtual void fromJson(QString jsonString) { QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8()); jObj = doc.object(); } virtual void fromJsonObject(QJsonObject json) { jObj = json; } virtual bool isSet() const { return false; } virtual bool isValid() const { return true; } private: QJsonObject jObj; }; } // namespace RespExtServer Q_DECLARE_METATYPE(RespExtServer::OAIObject) #endif // OAI_OBJECT_H ================================================ FILE: src/modules/extension-server/client/OAIServerConfiguration.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * Representing a Server configuration. */ #ifndef OAI_SERVERVCONFIGURATION_H #define OAI_SERVERVCONFIGURATION_H #include #include #include #include #include #include "OAIServerVariable.h" namespace RespExtServer { class OAIServerConfiguration { public: /** * @param url A URL to the target host. * @param description A description of the host designated by the URL. * @param variables A map between a variable name and its value. The value is used for substitution in the server's URL template. */ OAIServerConfiguration(const QUrl &url, const QString &description, const QMap &variables) : _description(description), _variables(variables), _url(url){} OAIServerConfiguration(){} ~OAIServerConfiguration(){} /** * Format URL template using given variables. * * @param variables A map between a variable name and its value. * @return Formatted URL. */ QString URL() { QString url = _url.toString(); if(!_variables.empty()){ // go through variables and replace placeholders for (auto const& v : _variables.keys()) { QString name = v; OAIServerVariable serverVariable = _variables.value(v); QString value = serverVariable._defaultValue; if (!serverVariable._enumValues.empty() && !serverVariable._enumValues.contains(value)) { throw std::runtime_error(QString("The variable " + name + " in the server URL has invalid value " + value + ".").toUtf8()); } QRegularExpression regex(QString("\\{" + name + "\\}")); url = url.replace(regex, value); } return url; } return url; } int setDefaultValue(const QString &variable,const QString &value){ if(_variables.contains(variable)) return _variables[variable].setDefaultValue(value); return -1; } QString _description; QMap _variables; QUrl _url; }; } // namespace RespExtServer #endif // OAI_SERVERVCONFIGURATION_H ================================================ FILE: src/modules/extension-server/client/OAIServerVariable.h ================================================ /** * RESP.app Extension server * RESP.app Extension Server API allows you to extend RESP.app with your custom data formatters * * The version of the OpenAPI document: 2022.0-preview1 * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * Representing a Server Variable for server URL template substitution. */ #ifndef OAI_SERVERVARIABLE_H #define OAI_SERVERVARIABLE_H #include #include namespace RespExtServer { class OAIServerVariable { public: /** * @param description A description for the server variable. * @param defaultValue The default value to use for substitution. * @param enumValues An enumeration of string values to be used if the substitution options are from a limited set. */ OAIServerVariable(const QString &description, const QString &defaultValue, const QSet &enumValues) : _defaultValue(defaultValue), _description(description), _enumValues(enumValues){} OAIServerVariable(){} ~OAIServerVariable(){} int setDefaultValue(const QString& value){ if( _enumValues.contains(value)){ _defaultValue = value; return 0; } return -2; } QString getDefaultValue(){return _defaultValue;} QSet getEnumValues(){return _enumValues;} QString _defaultValue; QString _description; QSet _enumValues; }; } // namespace RespExtServer #endif // OAI_SERVERVARIABLE_H ================================================ FILE: src/modules/extension-server/client/client.pri ================================================ QT += network HEADERS += \ # Models $${PWD}/OAIDataFormatter.h \ $${PWD}/OAIDecodePayload.h \ $${PWD}/OAIEncodePayload.h \ $${PWD}/OAIInline_response_400.h \ # APIs $${PWD}/OAIDefaultApi.h \ # Others $${PWD}/OAIHelpers.h \ $${PWD}/OAIHttpRequest.h \ $${PWD}/OAIObject.h \ $${PWD}/OAIEnum.h \ $${PWD}/OAIHttpFileElement.h \ $${PWD}/OAIServerConfiguration.h \ $${PWD}/OAIServerVariable.h \ $${PWD}/OAIOauth.h SOURCES += \ # Models $${PWD}/OAIDataFormatter.cpp \ $${PWD}/OAIDecodePayload.cpp \ $${PWD}/OAIEncodePayload.cpp \ $${PWD}/OAIInline_response_400.cpp \ # APIs $${PWD}/OAIDefaultApi.cpp \ # Others $${PWD}/OAIHelpers.cpp \ $${PWD}/OAIHttpRequest.cpp \ $${PWD}/OAIHttpFileElement.cpp \ $${PWD}/OAIOauth.cpp ================================================ FILE: src/modules/extension-server/dataformattermanager.cpp ================================================ #include "dataformattermanager.h" #include #include #include #include #include #include #include #include "app/models/configmanager.h" #include "client/OAIDefaultApi.h" RespExtServer::DataFormattersManager::DataFormattersManager(QQmlApplicationEngine &engine) : m_engine(engine), m_api(new RespExtServer::OAIDefaultApi()) { QSettings settings; int requestTimeout = settings.value("app/extensionServerRequestTimeout", 10).toInt() * 1000; m_api->setTimeOut(requestTimeout); QObject::connect(m_api.data(), &OAIDefaultApi::dataFormattersGetSignal, this, &DataFormattersManager::onLoaded); QObject::connect(m_api.data(), &OAIDefaultApi::dataFormattersGetSignalE, this, &DataFormattersManager::onLoadingError); QObject::connect(m_api.data(), &OAIDefaultApi::dataFormattersIdDecodePostSignalFull, this, &DataFormattersManager::onDecoded); QObject::connect(m_api.data(), &OAIDefaultApi::dataFormattersIdEncodePostSignalFull, this, &DataFormattersManager::onEncoded); QObject::connect(m_api.data(), &OAIDefaultApi::dataFormattersIdDecodePostSignalEFull, this, &DataFormattersManager::onDecodeError); QObject::connect(m_api.data(), &OAIDefaultApi::dataFormattersIdEncodePostSignalEFull, this, &DataFormattersManager::onEncodeError); } void RespExtServer::DataFormattersManager::loadFormatters() { m_api->setNewServerForAllOperations(extServerUrl()); QSettings settings; QString user = settings.value("app/extensionServerUser", QString()).toString(); QString password = settings.value("app/extensionServerPassword", QString()).toString(); if (!user.isEmpty() && !password.isEmpty()) { m_api->setUsername(user); m_api->setPassword(password); } m_api->dataFormattersGet(); } int RespExtServer::DataFormattersManager::rowCount(const QModelIndex &) const { return m_formattersData.size(); } QVariant RespExtServer::DataFormattersManager::data(const QModelIndex &index, int role) const { if (!(0 <= index.row() && index.row() < rowCount())) { return QVariant(); } auto data = m_formattersData[index.row()]; if (role == name) { return data.getName(); } else if (role == id) { return data.getId(); } else if (role == keyTypes) { return data.getKeyTypes(); } else if (role == magicHeader) { return data.getMagicHeader(); } else if (role == readOnly) { return data.isReadOnly(); } return QVariant(); } QHash RespExtServer::DataFormattersManager::roleNames() const { QHash roles; roles[id] = "id"; roles[name] = "name"; roles[keyTypes] = "keyTypes"; roles[magicHeader] = "magicHeader"; roles[readOnly] = "readOnly"; return roles; } void RespExtServer::DataFormattersManager::setUrl(const QString &path) { m_extServerUrl = path; } void RespExtServer::DataFormattersManager::decode(const QString &formatterId, const QByteArray &data, QVariant context, QJSValue jsCallback) { if (!m_mapping.contains(formatterId)) { emit error(QCoreApplication::translate("RESP", "Can't find formatter: %1") .arg(formatterId)); return; } if (!jsCallback.isCallable() || !context.canConvert()) { emit error(QCoreApplication::translate("RESP", "Invalid callback")); return; } auto requestContext = context.toMap(); m_context = FormatterContext{jsCallback, formatterId}; OAIDecodePayload payload; payload.setData(data.toBase64()); payload.setRedisKeyName(requestContext["redis-key-name"].toByteArray()); payload.setRedisKeyType(requestContext["redis-key-type"].toString()); m_api->dataFormattersIdDecodePost(formatterId, payload); } void RespExtServer::DataFormattersManager::isValid(const QString &formatterId, const QByteArray &data, QVariant context, QJSValue jsCallback) { // TODO: Check magic header if any Q_UNUSED(context); Q_UNUSED(formatterId); Q_UNUSED(data); jsCallback.call(QJSValueList{true}); } void RespExtServer::DataFormattersManager::encode(const QString &formatterId, const QByteArray &data, QVariant context, QJSValue jsCallback) { if (!m_mapping.contains(formatterId)) { emit error(QCoreApplication::translate("RESP", "Can't find formatter: %1") .arg(formatterId)); return; } m_context = FormatterContext{jsCallback, formatterId}; OAIEncodePayload payload; payload.setData(data.toBase64()); auto requestContext = QJsonDocument::fromVariant(context); if (requestContext.isObject()) { OAIObject metadata; metadata.fromJsonObject(requestContext.object()); payload.setMetadata(metadata); } m_api->dataFormattersIdEncodePost(formatterId, payload); } QVariantList RespExtServer::DataFormattersManager::getPlainList() { QList r; foreach (auto v, m_formattersData) { r.append(v.asJsonObject().toVariantMap()); } return r; } QString RespExtServer::DataFormattersManager::extServerUrl() { if (!m_extServerUrl.isEmpty()) { return m_extServerUrl; } QSettings settings; return settings.value("app/extensionServerUrl", QString()).toString(); } bool RespExtServer::DataFormattersManager::isInstalled(const QString &name) { return m_mapping.contains(name); } void RespExtServer::DataFormattersManager::onLoaded( QList summary) { qDebug() << "Formatters loaded from extension server" << summary.size(); emit layoutAboutToBeChanged(); m_formattersData = summary; fillMapping(); emit layoutChanged(); emit loaded(); } void RespExtServer::DataFormattersManager::onLoadingError( QList, QNetworkReply::NetworkError, QString error_str) { emit error( QCoreApplication::translate( "RESP", "Can't load list of available formatters from extension server: %1") .arg(error_str)); } void RespExtServer::DataFormattersManager::onDecoded( OAIHttpRequestWorker *worker, QString) { if (!worker || !m_context.isValid()) return; auto headers = worker->getResponseHeaders(); QString format{"plain"}; auto decoded = QString::fromUtf8(worker->response); if (headers.contains("Content-Type")) { if (headers["Content-Type"].toLower() == "application/json") { format = "json"; } else if (headers["Content-Type"].toLower().startsWith("image")) { format = "image"; decoded = QString("data:%1;base64,%2") .arg(headers["Content-Type"]) .arg(QString(worker->response.toBase64())); } } auto formatter = m_formattersData[m_mapping[m_context.formatterId]]; m_context.jsCallback.call( QJSValueList{QString(), decoded, formatter.isReadOnly(), format}); } void RespExtServer::DataFormattersManager::onEncoded( OAIHttpRequestWorker *worker, QString) { if (!worker || !m_context.isValid()) return; auto encoded = m_engine.toScriptValue(worker->response); m_context.jsCallback.call(QJSValueList{QString(), encoded}); } void RespExtServer::DataFormattersManager::onDecodeError( OAIHttpRequestWorker *worker, QNetworkReply::NetworkError, QString error_str) { if (!worker || !m_context.isValid()) return; auto formatter = m_formattersData[m_mapping[m_context.formatterId]]; m_context.jsCallback.call(QJSValueList{error_str, QString(), true, "plain"}); } void RespExtServer::DataFormattersManager::onEncodeError( OAIHttpRequestWorker *worker, QNetworkReply::NetworkError, QString error_str) { if (!worker || !m_context.isValid()) return; emit error(QCoreApplication::translate("RESP", "Can't encode value: %1") .arg(error_str)); } void RespExtServer::DataFormattersManager::fillMapping() { int index = 0; for (const auto &f : qAsConst(m_formattersData)) { m_mapping[f.getId()] = index; index++; } } ================================================ FILE: src/modules/extension-server/dataformattermanager.h ================================================ #pragma once #include "client/OAIDataFormatter.h" #include "client/OAIHttpRequest.h" #include #include #include #include #include namespace RespExtServer { class OAIDefaultApi; class DataFormattersManager : public QAbstractListModel { Q_OBJECT public: enum Roles { name = Qt::UserRole + 1, id, keyTypes, magicHeader, readOnly }; public: DataFormattersManager(QQmlApplicationEngine& engine); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role) const override; QHash roleNames() const override; void setUrl(const QString& path); signals: void error(const QString& msg); void loaded(); public: Q_INVOKABLE void loadFormatters(); Q_INVOKABLE void decode(const QString& formatterId, const QByteArray& data, QVariant context, QJSValue jsCallback); Q_INVOKABLE void isValid(const QString& formatterName, const QByteArray& data, QVariant context, QJSValue jsCallback); Q_INVOKABLE void encode(const QString& formatterId, const QByteArray& data, QVariant context, QJSValue jsCallback); Q_INVOKABLE QVariantList getPlainList(); Q_INVOKABLE QString extServerUrl(); Q_INVOKABLE bool isInstalled(const QString& name); protected slots: void onLoaded(QList summary); void onLoadingError(QList summary, QNetworkReply::NetworkError error_type, QString error_str); void onDecoded(OAIHttpRequestWorker *worker, QString summary); void onEncoded(OAIHttpRequestWorker *worker, QString summary); void onDecodeError(OAIHttpRequestWorker *worker, QNetworkReply::NetworkError error_type, QString error_str); void onEncodeError(OAIHttpRequestWorker *, QNetworkReply::NetworkError, QString error_str); private: void fillMapping(); struct FormatterContext { QJSValue jsCallback = QJSValue(); QString formatterId = QString(); bool decodeValidation = false; FormatterContext() {} FormatterContext(QJSValue c, QString f) : jsCallback(c), formatterId(f) {} bool isValid() { return jsCallback.isCallable() && !formatterId.isEmpty(); } }; private: QQmlApplicationEngine& m_engine; QList m_formattersData; QHash m_mapping; QString m_extServerUrl; QSharedPointer m_api; FormatterContext m_context; }; } ================================================ FILE: src/modules/extension-server/generate_client.sh ================================================ #!/bin/bash openapi-generator generate -i server_spec.yaml -g cpp-qt-client --additional-properties=cppNamespace=RespExtServer -o . ================================================ FILE: src/modules/server-actions/serverstatsmodel.cpp ================================================ #include "serverstatsmodel.h" #include #include ServerStats::Model::Model(QSharedPointer connection, int dbIndex, QList) : TabModel(connection, dbIndex) { m_serverInfoUpdateTimer.setInterval(5000); m_serverInfoUpdateTimer.setSingleShot(false); m_slowLogUpdateTimer.setInterval(5000); m_slowLogUpdateTimer.setSingleShot(false); m_clientsUpdateTimer.setInterval(5000); m_clientsUpdateTimer.setSingleShot(false); m_pubSubMonitorConnection = connection->clone(); QObject::connect(&m_serverInfoUpdateTimer, &QTimer::timeout, this, &Model::srvInfoCallback); QObject::connect(&m_slowLogUpdateTimer, &QTimer::timeout, this, &Model::slowLogCallback); QObject::connect(&m_clientsUpdateTimer, &QTimer::timeout, this, &Model::clientsCallback); QObject::connect(this, &TabModel::initialized, [this]() { srvInfoCallback(); m_serverInfoUpdateTimer.start(); }); } ServerStats::Model::~Model() { m_serverInfoUpdateTimer.stop(); m_slowLogUpdateTimer.stop(); m_clientsUpdateTimer.stop(); } QString ServerStats::Model::getName() const { return QCoreApplication::translate("RESP", "Server %0") .arg(m_connection->getConfig().name()); } QVariantMap ServerStats::Model::serverInfo() const { return m_serverInfo; } QVariant ServerStats::Model::slowLog() const { return m_slowLog; } QVariant ServerStats::Model::clients() const { return m_clients; } QVariant ServerStats::Model::pubSubChannels() const { QVariantList r; for (QByteArray ch : m_pubSubChannels) { r.append(QVariant(ch)); } return r; } bool ServerStats::Model::refreshSlowLog() const { return m_slowLogUpdateTimer.isActive(); } void ServerStats::Model::setRefreshSlowLog(bool v) { if (refreshSlowLog() != v && refreshSlowLog()) m_slowLogUpdateTimer.stop(); if (refreshSlowLog() != v && !refreshSlowLog()) { slowLogCallback(); m_slowLogUpdateTimer.start(); } } bool ServerStats::Model::refreshClients() const { return m_clientsUpdateTimer.isActive(); } void ServerStats::Model::setRefreshClients(bool v) { if (refreshClients() != v && refreshClients()) m_clientsUpdateTimer.stop(); if (refreshClients() != v && !refreshClients()) { clientsCallback(); m_clientsUpdateTimer.start(); } } bool ServerStats::Model::refreshPubSubMonitor() const { return m_pubSubMonitorConnection->isConnected(); } void ServerStats::Model::setRefreshPubSubMonitor(bool v) { if (m_pubSubMonitorConnection->isConnected() && !v) { m_pubSubMonitorConnection->disconnect(); return; } if (!m_pubSubMonitorConnection->isConnected() && v) { m_pubSubMonitorConnection->cmd( {"PSUBSCRIBE", "*"}, this, -1, [this](const RedisClient::Response& result) { if (result.type() != RedisClient::Response::Array) { return; } QVariantList msg = result.value().toList(); if (msg.size() == 4) { m_pubSubChannels.insert(msg[2].toByteArray()); } emit pubSubChannelsChanged(); }, [this](const QString& e) { cmdErrorHander(e); }); } } void ServerStats::Model::subscribeToChannel(const QString &c) { emit openConsoleTerminal(m_connection, m_dbIndex, true, {"SUBSCRIBE", c.toUtf8()}); } void ServerStats::Model::monitorCommands() { emit openConsoleTerminal(m_connection, m_dbIndex, true, {"MONITOR"}); } void ServerStats::Model::openTerminal() { emit openConsoleTerminal(m_connection, m_dbIndex, true, {}); } void ServerStats::Model::cmdErrorHander(const QString& err) { emit error(err); } void ServerStats::Model::srvInfoCallback() { m_connection->cmd( {"INFO", "all"}, this, -1, [this](const RedisClient::Response& r) { m_serverInfo = RedisClient::ServerInfo::fromString( QString::fromUtf8(r.value().toByteArray())) .parsed.toVariantMap(); emit serverInfoChanged(); }, [this](const QString& e) { cmdErrorHander(e); }); } void ServerStats::Model::slowLogCallback() { m_connection->cmd( {"SLOWLOG", "GET", "15"}, this, -1, [this](const RedisClient::Response& r) { QVariantList processed; for (QVariant item : r.value().toList()) { auto itemList = item.toList(); QVariantMap row; row.insert("time", itemList[1]); row.insert("exec_time", itemList[2]); row.insert("cmd", itemList[3]); processed.append(row); } m_slowLog = processed; emit slowLogChanged(); }, [this](const QString& e) { cmdErrorHander(e); }); } void ServerStats::Model::clientsCallback() { m_connection->cmd( {"CLIENT", "LIST"}, this, -1, [this](const RedisClient::Response& r) { QVariant result = r.value(); QStringList lines = result.toString().split("\n"); QVariantList parsedClients; for (auto rawLine : lines) { QStringList lineParts = rawLine.split(" "); QVariantMap parsed; for (auto linePart : lineParts) { QStringList keyAndVal = linePart.split("="); if (keyAndVal.size() > 1) { parsed.insert(keyAndVal[0], keyAndVal[1]); } else if (linePart.size() > 0) { parsed.insert(keyAndVal[0], ""); } } if (parsed.size() > 0) parsedClients.append(parsed); } m_clients = parsedClients; emit clientsChanged(); }, [this](const QString& e) { cmdErrorHander(e); }); } ================================================ FILE: src/modules/server-actions/serverstatsmodel.h ================================================ #pragma once #include "common/tabviewmodel.h" #include "exception.h" namespace ServerStats { class Model : public TabModel { Q_OBJECT ADD_EXCEPTION Q_PROPERTY(QVariantMap serverInfo READ serverInfo NOTIFY serverInfoChanged) Q_PROPERTY(QVariant slowLog READ slowLog NOTIFY slowLogChanged) Q_PROPERTY(bool refreshSlowLog READ refreshSlowLog WRITE setRefreshSlowLog) Q_PROPERTY(QVariant clients READ clients NOTIFY clientsChanged) Q_PROPERTY(bool refreshClients READ refreshClients WRITE setRefreshClients) Q_PROPERTY( QVariant pubSubChannels READ pubSubChannels NOTIFY pubSubChannelsChanged) Q_PROPERTY(bool refreshPubSubMonitor READ refreshPubSubMonitor WRITE setRefreshPubSubMonitor) public: Model(QSharedPointer connection, int dbIndex, QList); ~Model() override; QString getName() const override; QVariantMap serverInfo() const; QVariant slowLog() const; QVariant clients() const; QVariant pubSubChannels() const; bool refreshSlowLog() const; void setRefreshSlowLog(bool v); bool refreshClients() const; void setRefreshClients(bool v); bool refreshPubSubMonitor() const; void setRefreshPubSubMonitor(bool v); Q_INVOKABLE void subscribeToChannel(const QString& c); Q_INVOKABLE void monitorCommands(); Q_INVOKABLE void openTerminal(); signals: void serverInfoChanged(); void slowLogChanged(); void clientsChanged(); void pubSubChannelsChanged(); void openConsoleTerminal(QSharedPointer c, int db, bool inNewTab, QList cmd); protected: void cmdErrorHander(const QString& err); protected slots: void srvInfoCallback(); void slowLogCallback(); void clientsCallback(); private: QTimer m_serverInfoUpdateTimer; QTimer m_slowLogUpdateTimer; QTimer m_clientsUpdateTimer; QSharedPointer m_pubSubMonitorConnection; QVariantMap m_serverInfo; QVariant m_slowLog; QVariant m_clients; QSet m_pubSubChannels; }; } // namespace ServerStats ================================================ FILE: src/modules/value-editor/abstractkeyfactory.h ================================================ #pragma once #include #include #include #include #include "keymodel.h" namespace RedisClient { class Connection; } namespace ValueEditor { class AbstractKeyFactory { public: virtual ~AbstractKeyFactory() {} virtual void loadKey( QSharedPointer connection, QByteArray keyFullPath, int dbIndex, std::function, const QString&)> callback) = 0; }; } // namespace ValueEditor ================================================ FILE: src/modules/value-editor/embeddedformattersmanager.cpp ================================================ #include "embeddedformattersmanager.h" #include #include #include #include #include #include #include #include "app/models/configmanager.h" ValueEditor::EmbeddedFormattersManager::EmbeddedFormattersManager() : m_python(nullptr) {} void ValueEditor::EmbeddedFormattersManager::init(QSharedPointer p) { if (!p) { emit error("Failed to load python"); return; } m_python = p; QObject::connect(m_python.data(), &QPython::error, this, &EmbeddedFormattersManager::error); } void ValueEditor::EmbeddedFormattersManager::loadFormattersModule( QJSValue callback) { if (!m_python) { qWarning() << "EmbeddedFormattersManager is not ready"; return; } m_python->importModule("formatters", callback); } void ValueEditor::EmbeddedFormattersManager::loadFormatters(QJSValue callback) { pythonCall("formatters.get_formatters_list", QVariantList(), callback); } void ValueEditor::EmbeddedFormattersManager::decode( const QString &formatterName, const QByteArray &data, QJSValue jsCallback) { pythonCall("formatters.decode", QVariantList{formatterName, data}, jsCallback); } void ValueEditor::EmbeddedFormattersManager::isValid( const QString &formatterName, const QByteArray &data, QJSValue jsCallback) { pythonCall("formatters.validate", QVariantList{formatterName, data}, jsCallback); } void ValueEditor::EmbeddedFormattersManager::encode( const QString &formatterName, const QByteArray &data, QJSValue jsCallback) { pythonCall("formatters.encode", QVariantList{formatterName, data}, jsCallback); } void ValueEditor::EmbeddedFormattersManager::pythonCall( const QString &callable_name, const QVariantList &args, QJSValue jsCallback) { if (!m_python) { qWarning() << "EmbeddedFormattersManager is not ready"; return; } m_python->call(callable_name, args, jsCallback); } ================================================ FILE: src/modules/value-editor/embeddedformattersmanager.h ================================================ #pragma once #include #include #include class QPython; namespace ValueEditor { class EmbeddedFormattersManager : public QObject { Q_OBJECT public: enum Roles { name = Qt::UserRole + 1, version, description, cmd }; public: EmbeddedFormattersManager(); void init(QSharedPointer p); signals: void error(const QString& msg); public: Q_INVOKABLE void loadFormattersModule(QJSValue callback); Q_INVOKABLE void loadFormatters(QJSValue callback); Q_INVOKABLE void decode(const QString& formatterName, const QByteArray& data, QJSValue jsCallback); Q_INVOKABLE void isValid(const QString& formatterName, const QByteArray& data, QJSValue jsCallback); Q_INVOKABLE void encode(const QString& formatterName, const QByteArray& data, QJSValue jsCallback); protected: void pythonCall(const QString& callable_name, const QVariantList& args, QJSValue jsCallback); private: QSharedPointer m_python; }; } // namespace ValueEditor ================================================ FILE: src/modules/value-editor/keymodel.h ================================================ #pragma once #include #include #include #include #include #include #include "exception.h" namespace ValueEditor { class ModelSignals : public QObject { Q_OBJECT public: ModelSignals() {} signals: void removed(); void error(const QString&); }; class Model : public QEnableSharedFromThis { public: typedef std::function Callback; Model() {} virtual QString getKeyName() = 0; virtual QString getKeyTitle(int limit = -1) = 0; virtual QString type() = 0; virtual long long getTTL() = 0; virtual QStringList getColumnNames() = 0; virtual QHash getRoles() = 0; virtual QVariant getData(int rowIndex, int dataRole) = 0; virtual void setKeyName(const QByteArray&, Callback) = 0; // async virtual void setTTL(const long long, Callback) = 0; // async virtual void persistKey(Callback) = 0; // async virtual void removeKey(Callback) = 0; // rows operations virtual void addRow(const QVariantMap&, Callback) = 0; virtual void updateRow(int rowIndex, const QVariantMap&, Callback) = 0; // async virtual unsigned long rowsCount() = 0; //filters virtual QVariant filter(const QString& key) const = 0; virtual void setFilter(const QString&, QVariant) = 0; typedef std::function LoadRowsCallback; virtual void loadRows(QVariant rowStart, unsigned long count, LoadRowsCallback c) = 0; // async virtual void clearRowCache() = 0; virtual void removeRow(int, Callback) = 0; // async virtual bool isRowLoaded(int) = 0; virtual bool isMultiRow() const = 0; virtual void loadRowsCount(Callback callback) = 0; virtual QSharedPointer getConnector() const = 0; virtual QSharedPointer getConnection() const = 0; virtual unsigned int dbIndex() const = 0; virtual QString getDefaultFormatter() const = 0; virtual ~Model() {} }; } // namespace ValueEditor ================================================ FILE: src/modules/value-editor/largetextmodel.cpp ================================================ #include "largetextmodel.h" #include ValueEditor::LargeTextWrappingModel::LargeTextWrappingModel(const QString &text, uint chunkSize) : m_chunkSize(chunkSize) { setText(text); } ValueEditor::LargeTextWrappingModel::~LargeTextWrappingModel() {} QHash ValueEditor::LargeTextWrappingModel::roleNames() const { QHash roles; roles[Qt::UserRole + 1] = "value"; return roles; } int ValueEditor::LargeTextWrappingModel::rowCount(const QModelIndex &) const { return m_textRows.size(); } QVariant ValueEditor::LargeTextWrappingModel::data(const QModelIndex &index, int role) const { if (!isIndexValid(index)) return QVariant(); if (role == Qt::UserRole + 1) { return m_textRows[index.row()]; } return QVariant(); } void ValueEditor::LargeTextWrappingModel::setText(const QString &text) { m_textRows.reserve(text.size() / m_chunkSize); for (uint chunkIndex = 0; chunkIndex < text.size() / m_chunkSize + 1; chunkIndex++) { m_textRows.append(text.mid(chunkIndex * m_chunkSize, m_chunkSize)); } } void ValueEditor::LargeTextWrappingModel::cleanUp() { emit beginRemoveRows(QModelIndex(), 0, rowCount() - 1); m_textRows.clear(); emit endRemoveRows(); } QString ValueEditor::LargeTextWrappingModel::getText() { QString result; result.reserve(m_textRows.size() * m_chunkSize); for (auto textRow : m_textRows) { result.append(textRow); } return result; } void ValueEditor::LargeTextWrappingModel::setTextChunk(uint row, QString text) { if (row < m_textRows.size()) { m_textRows[row] = text; emit dataChanged(createIndex(row, 0), createIndex(row, 0)); } } /** * @brief ValueEditor::LargeTextWrappingModel::searchText * @param p * @param from * @param regex * @return * 1: TargetTextView * 2: Raw Position * 3: Relative position for search in TargetTextView * 4. Length */ QVariantList ValueEditor::LargeTextWrappingModel::searchText(QString p, int from, bool regex) { QString text = getText(); if (from < 0) { from = 0; } qDebug() << "Search params:" << p << from << regex; int res; int length = 0; if (regex) { auto rx = QRegExp(p); res = text.indexOf(rx, from); length = rx.matchedLength(); } else { res = text.indexOf(p, from, Qt::CaseInsensitive); length = p.size(); } if (res == -1) { return QVariantList {-1, -1, -1, -1}; } else { int row = (int)res / m_chunkSize; return QVariantList {row, res, res % m_chunkSize, length}; } } bool ValueEditor::LargeTextWrappingModel::isIndexValid( const QModelIndex &index) const { return 0 <= index.row() && index.row() < rowCount(); } ================================================ FILE: src/modules/value-editor/largetextmodel.h ================================================ #pragma once #include #include #include #include namespace ValueEditor { class LargeTextWrappingModel : public QAbstractListModel { // TODO(u_glide): Process out of memory exceptions Q_OBJECT public: LargeTextWrappingModel(const QString &text = QString(), uint chunkSize = 10000); ~LargeTextWrappingModel(); QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; void setText(const QString &text); public slots: void cleanUp(); QString getText(); void setTextChunk(uint row, QString text); QVariantList searchText(QString p, int from = 0, bool regex=false); private: bool isIndexValid(const QModelIndex &index) const; private: uint m_chunkSize; QList m_textRows; }; } // namespace ValueEditor ================================================ FILE: src/modules/value-editor/syntaxhighlighter.cpp ================================================ #include "syntaxhighlighter.h" #include "textcharformat.h" SyntaxHighlighter::SyntaxHighlighter( QObject* parent ) : QSyntaxHighlighter(parent), m_TextDocument(nullptr) { } void SyntaxHighlighter::highlightBlock( const QString &text ) { emit highlightBlock( QVariant(text) ); } QQuickTextDocument* SyntaxHighlighter::textDocument() const { return m_TextDocument; } void SyntaxHighlighter::setTextDocument( QQuickTextDocument* textDocument ) { if (textDocument == m_TextDocument) { return; } m_TextDocument = textDocument; QTextDocument* doc = m_TextDocument->textDocument(); setDocument(doc); emit textDocumentChanged(); } void SyntaxHighlighter::setFormat( int start, int count, const QVariant& format ) { TextCharFormat* charFormat = qvariant_cast( format ); if ( charFormat ) { QSyntaxHighlighter::setFormat( start, count, *charFormat ); return; } if ( format.canConvert(QVariant::Color) ) { QSyntaxHighlighter::setFormat( start, count, format.value() ); return; } if ( format.canConvert(QVariant::Font) ) { QSyntaxHighlighter::setFormat( start, count, format.value() ); return; } } ================================================ FILE: src/modules/value-editor/syntaxhighlighter.h ================================================ #pragma once #include #include #include #include /*** * Based on https://github.com/stephenquan/QtSyntaxHighlighterApp * * Original code is licensed under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * */ class SyntaxHighlighter : public QSyntaxHighlighter { Q_OBJECT Q_PROPERTY(QQuickTextDocument* textDocument READ textDocument WRITE setTextDocument NOTIFY textDocumentChanged) public: SyntaxHighlighter(QObject* parent = nullptr); Q_INVOKABLE void setFormat(int start, int count, const QVariant& format); signals: void textDocumentChanged(); void highlightBlock(const QVariant& text); protected: QQuickTextDocument* m_TextDocument; QQuickTextDocument* textDocument() const; void setTextDocument(QQuickTextDocument* textDocument); virtual void highlightBlock(const QString &text); }; ================================================ FILE: src/modules/value-editor/tabsmodel.cpp ================================================ #include "tabsmodel.h" #include #include #include #include #include #include #include "app/events.h" #include "connections-tree/items/keyitem.h" #include "value-editor/valueviewmodel.h" #define TAB_NAME_LIMIT 30 ValueEditor::TabsModel::TabsModel(QSharedPointer keyFactory, QSharedPointer events) : m_keyFactory(keyFactory), m_events(events), m_currentTabIndex(0) {} ValueEditor::TabsModel::~TabsModel() { m_viewModels.clear(); } void ValueEditor::TabsModel::openTab( QSharedPointer connection, QSharedPointer key, bool inNewTab) { auto viewModel = createViewModel( QString(QCoreApplication::translate("RESP", "Loading key: %1 from db %2")) .arg(QString::fromUtf8(key->getFullPath())) .arg(key->getDbIndex()), key.toWeakRef()); QSharedPointer conn; if (inNewTab || m_viewModels.count() == 0) { beginInsertRows(QModelIndex(), m_viewModels.count(), m_viewModels.count()); m_viewModels.append(viewModel); endInsertRows(); } else { emit layoutAboutToBeChanged(); if (!(0 <= m_currentTabIndex && m_currentTabIndex < rowCount())) { m_currentTabIndex = rowCount() - 1; } auto oldModel = m_viewModels[m_currentTabIndex]; m_viewModels.replace(m_currentTabIndex, viewModel); emit layoutChanged(); emit replaceTab(m_currentTabIndex); auto keyModel = oldModel->model(); bool reuseConnection = (keyModel && keyModel->getConnection()->getConfig().id() == connection->getConfig().id()); if (reuseConnection) { conn = keyModel->getConnection(); } oldModel.clear(); } auto viewModelWeekRef = viewModel.toWeakRef(); auto loadingHandler = [this, viewModelWeekRef](QSharedPointer keyModel, const QString& error) { if (keyModel.isNull() || !error.isEmpty()) { emit tabError(-1, QString("%1:\n%2") .arg(QCoreApplication::translate( "RESP", "Cannot open value tab")) .arg(error)); return; } auto viewModel = viewModelWeekRef.toStrongRef(); if (viewModel) { viewModel->setModel(keyModel); } }; auto callbackWrapper = [loadingHandler](QSharedPointer keyModel, const QString& error) { QTimer::singleShot(1, [=]() { loadingHandler(keyModel, error); }); }; if (!conn) { conn = connection->clone(); conn->disableAutoConnect(); m_events->registerLoggerForConnection(*conn); } viewModel->setConnection(conn); connect(conn.data(), &RedisClient::Connection::shutdownStart, this, [this, viewModel](){ if (!viewModel) return; viewModel->setTabError(QCoreApplication::translate("RESP", "Connection error")); int modelIndex = m_viewModels.indexOf(viewModel); if (modelIndex != -1) { emit dataChanged(index(modelIndex, 0), index(modelIndex, 0)); } }); try { QtConcurrent::run([this, conn, key, viewModelWeekRef, callbackWrapper]() { if (!conn->isConnected()) conn->connect(); m_keyFactory->loadKey(conn, key->getFullPath(), key->getDbIndex(), callbackWrapper); }); } catch (...) { emit tabError(-1, QCoreApplication::translate( "RESP", "Connection error. Can't open value tab. ")); } } void ValueEditor::TabsModel::closeDbKeys( QSharedPointer connection, int dbIndex, const QRegExp& filter) { for (int index = 0; 0 <= index && index < m_viewModels.size(); index++) { auto model = m_viewModels.at(index)->model(); if (!model) continue; bool tabMatch = (model->getConnection()->getConfig().id() == connection->getConfig().id() && model->dbIndex() == dbIndex && model->getKeyName().contains(filter)); if (tabMatch) { beginRemoveRows(QModelIndex(), index, index); auto model = m_viewModels[index]; m_viewModels.removeAt(index); endRemoveRows(); index--; model.clear(); } } } QModelIndex ValueEditor::TabsModel::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); if (row < 0 || column < 0) return QModelIndex(); return createIndex(row, 0); } int ValueEditor::TabsModel::rowCount(const QModelIndex&) const { return m_viewModels.count(); } QVariant ValueEditor::TabsModel::data(const QModelIndex& index, int role) const { if (!isIndexValid(index)) return QVariant(); QSharedPointer model = m_viewModels.at(index.row())->model(); if (!model) { switch (role) { case keyIndex: return index.row(); case showLoader: return true; case tabName: return m_viewModels.at(index.row())->tabLoadingTitle(); } return QVariant(); } switch (role) { case keyIndex: return index.row(); case keyNameRole: return model->getKeyName(); case tabName: return model->getKeyTitle(TAB_NAME_LIMIT); case keyTTL: return model->getTTL(); case keyType: return model->type(); case rowsCount: return (qlonglong)model->rowsCount(); case isMultiRow: return model->isMultiRow(); case showLoader: return false; case defaultFormatter: return model->getDefaultFormatter(); case keyModel: QObject* modelPtr = static_cast(m_viewModels.at(index.row()).data()); QQmlEngine::setObjectOwnership(modelPtr, QQmlEngine::CppOwnership); return QVariant::fromValue(modelPtr); } return QVariant(); } QHash ValueEditor::TabsModel::roleNames() const { QHash roles; roles[keyIndex] = "keyIndex"; roles[keyNameRole] = "keyName"; roles[keyTTL] = "keyTtl"; roles[keyType] = "keyType"; roles[isMultiRow] = "isMultiRow"; roles[rowsCount] = "keyRowsCount"; roles[keyModel] = "keyViewModel"; roles[showLoader] = "showLoader"; roles[tabName] = "tabName"; roles[defaultFormatter] = "defaultFormatter"; return roles; } void ValueEditor::TabsModel::closeTab(int i) { if (!isIndexValid(index(i, 0))) return; beginRemoveRows(QModelIndex(), i, i); auto model = m_viewModels[i]; m_viewModels.removeAt(i); endRemoveRows(); model->close(); model.clear(); } void ValueEditor::TabsModel::setCurrentTab(int i) { if (0 <= i && i < rowCount()) { m_currentTabIndex = i; } } bool ValueEditor::TabsModel::isIndexValid(const QModelIndex& index) const { return 0 <= index.row() && index.row() < rowCount(); } void ValueEditor::TabsModel::tabChanged( QSharedPointer m) { int modelIndex = m_viewModels.lastIndexOf(m); if (modelIndex == -1) return; emit dataChanged(index(modelIndex, 0), index(modelIndex, 0)); } void ValueEditor::TabsModel::tabRemoved( QSharedPointer m) { int modelIndex = m_viewModels.lastIndexOf(m); if (modelIndex == -1) return; beginRemoveRows(QModelIndex(), modelIndex, modelIndex); auto oldModel = m_viewModels[modelIndex]; m_viewModels.removeAt(modelIndex); endRemoveRows(); oldModel->close(); oldModel.clear(); } QSharedPointer ValueEditor::TabsModel::createViewModel( const QString& loadingBanner, QWeakPointer key) { QSharedPointer viewModel = QSharedPointer( new ValueViewModel(loadingBanner), &QObject::deleteLater); auto wPtr = viewModel.toWeakRef(); connect(viewModel.data(), &ValueViewModel::rowsLoaded, this, [this, wPtr](int, int) { auto viewModel = wPtr.toStrongRef(); if (!wPtr) return; tabChanged(viewModel); }); connect(viewModel.data(), &ValueViewModel::modelLoaded, this, [this, wPtr]() { auto viewModel = wPtr.toStrongRef(); if (!viewModel) return; tabChanged(viewModel); }); connect(viewModel.data(), &ValueViewModel::keyRenamed, this, [this, wPtr, key] { auto viewModel = wPtr.toStrongRef(); if (!viewModel) return; tabChanged(viewModel); if (key && viewModel->model()) key.toStrongRef()->setFullPath( viewModel->model()->getKeyName().toUtf8()); }); connect(viewModel.data(), &ValueViewModel::keyTTLChanged, this, [this, wPtr] { auto viewModel = wPtr.toStrongRef(); if (!viewModel) return; tabChanged(viewModel); }); connect(viewModel.data(), &ValueViewModel::keyRemoved, this, [this, wPtr, key] { auto viewModel = wPtr.toStrongRef(); if (!viewModel) return; tabRemoved(viewModel); if (key) key.toStrongRef()->setRemoved(); }); return viewModel; } ================================================ FILE: src/modules/value-editor/tabsmodel.h ================================================ #pragma once #include #include #include #include #include #include #include "abstractkeyfactory.h" #include "valueviewmodel.h" class Events; namespace ConnectionsTree { class KeyItem; } namespace ValueEditor { class TabsModel : public QAbstractListModel { Q_OBJECT public: enum Roles { keyNameRole = Qt::UserRole + 1, keyIndex, keyTTL, keyType, isMultiRow, rowsCount, keyModel, showLoader, tabName, defaultFormatter }; public: TabsModel(QSharedPointer keyFactory, QSharedPointer events); ~TabsModel() override; QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; public: // methods exported to QML Q_INVOKABLE void closeTab(int i); Q_INVOKABLE void setCurrentTab(int i); signals: void tabError(int index, const QString& error); void replaceTab(int index); public slots: void openTab(QSharedPointer connection, QSharedPointer key, bool inNewTab); void closeDbKeys(QSharedPointer connection, int dbIndex, const QRegExp& filter); private: QList> m_viewModels; QSharedPointer m_keyFactory; QSharedPointer m_events; int m_currentTabIndex; bool isIndexValid(const QModelIndex& index) const; QSharedPointer createViewModel(const QString& loadingBanner, QWeakPointer key); void tabChanged(QSharedPointer m); void tabRemoved(QSharedPointer m); }; } // namespace ValueEditor ================================================ FILE: src/modules/value-editor/textcharformat.cpp ================================================ #include "textcharformat.h" TextCharFormat::TextCharFormat(QObject* parent) : QObject(parent) { } void TextCharFormat::setFont( const QFont& font ) { if ( font == QTextCharFormat::font() ) { return; } QTextCharFormat::setFont(font); emit fontChanged(); } QFont TextCharFormat::font() const { return QTextCharFormat::font(); } QVariant TextCharFormat::foreground() const { return QTextCharFormat::foreground().color(); } void TextCharFormat::setForeground( const QVariant& foreground ) { if ( foreground.canConvert() ) { QTextCharFormat::setForeground( QBrush( foreground.value< QColor >() ) ); emit foregroundChanged(); } } ================================================ FILE: src/modules/value-editor/textcharformat.h ================================================ #pragma once #include #include /*** * Based on https://github.com/stephenquan/QtSyntaxHighlighterApp * * Original code is licensed under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * */ class TextCharFormat : public QObject, public QTextCharFormat { Q_OBJECT Q_PROPERTY (QFont font READ font WRITE setFont NOTIFY fontChanged) Q_PROPERTY (QVariant foreground READ foreground WRITE setForeground NOTIFY foregroundChanged) public: TextCharFormat(QObject* parent = nullptr); signals: void fontChanged(); void foregroundChanged(); protected: void setFont(const QFont& font); QFont font() const; QVariant foreground() const; void setForeground(const QVariant& foreground); }; ================================================ FILE: src/modules/value-editor/valueviewmodel.cpp ================================================ #include "valueviewmodel.h" #include #include #include #include #include ValueEditor::ValueViewModel::ValueViewModel(const QString& loadingTitle) : BaseListModel(), m_model(nullptr), m_connection(nullptr), m_startFramePosition(0), m_lastLoadedRowFrameSize(0), m_singlePageMode(false), m_tabTitle(loadingTitle) {} int ValueEditor::ValueViewModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); if (!m_model) return 0; if (m_singlePageMode) { return m_model->rowsCount(); } else { return m_lastLoadedRowFrameSize; } } int ValueEditor::ValueViewModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); if (!m_model) return 0; return m_model->getColumnNames().size(); } QString ValueEditor::ValueViewModel::tabLoadingTitle() const { return m_tabTitle; } void ValueEditor::ValueViewModel::setTabError(const QString &t) { m_tabTitle = t; } bool ValueEditor::ValueViewModel::isModelLoaded() const { return !m_model.isNull(); } QVariant ValueEditor::ValueViewModel::data(const QModelIndex& index, int role) const { if (!isIndexValid(index)) return QVariant(); int mappedRole = role; if (role == Qt::DisplayRole && index.column() > 0) { mappedRole = m_model->getRoles().key(m_model->getColumnNames().at(index.column()).toLatin1()); } return m_model->getData(m_startFramePosition + index.row(), mappedRole); } QHash ValueEditor::ValueViewModel::roleNames() const { auto roles = m_model->getRoles(); roles.insert(Qt::DisplayRole, "display"); return roles; } QSharedPointer ValueEditor::ValueViewModel::model() { return m_model; } void ValueEditor::ValueViewModel::setModel(QSharedPointer model) { m_model = model; emit modelLoaded(); } void ValueEditor::ValueViewModel::setConnection(QSharedPointer c) { m_connection = c; } void ValueEditor::ValueViewModel::renameKey(const QString& newKeyName) { if (!m_model) { qWarning() << "Model is not loaded"; return; } m_model->setKeyName(printableStringToBinary(newKeyName), [this](const QString& err) { if (err.size() > 0) { emit error(err); return; } emit keyRenamed(); }); } void ValueEditor::ValueViewModel::setTTL(const QString& newTTL) { if (!m_model) { qWarning() << "Model is not loaded"; return; } m_model->setTTL(newTTL.toLong(), [this](const QString& err) { if (err.size() > 0) { emit error(err); return; } emit keyTTLChanged(); }); } void ValueEditor::ValueViewModel::persistKey() { if (!m_model) { qWarning() << "Model is not loaded"; return; } m_model->persistKey([this](const QString& err) { if (err.size() > 0) { emit error(err); return; } emit keyTTLChanged(); }); } void ValueEditor::ValueViewModel::removeKey() { if (!m_model) { qWarning() << "Model is not loaded"; return; } m_model->removeKey([this](const QString& err) { if (err.size() > 0) { emit error(err); return; } emit keyRemoved(); }); } void ValueEditor::ValueViewModel::close() { emit tabClosed(); } QVariantList ValueEditor::ValueViewModel::columnNames() { QVariantList result; if (!m_model) return result; foreach (QString str, m_model->getColumnNames()) { result.append(QVariant(str)); } return result; } void ValueEditor::ValueViewModel::reload() { if (!m_model) { qWarning() << "Model is not loaded"; return; } m_model->clearRowCache(); m_model->loadRowsCount([this](const QString& err) { if (err.size() > 0 || m_model->rowsCount() <= 0) { emit error( QCoreApplication::translate("RESP", "Cannot reload key value: %1") .arg(err)); return; } emit totalRowCountChanged(); emit pageSizeChanged(); loadRows(m_startFramePosition, m_model->rowsCount() < pageSize() ? m_model->rowsCount() : pageSize()); }); } void ValueEditor::ValueViewModel::setSinglePageMode(bool v) { m_singlePageMode = v; emit singlePageModeChanged(); } bool ValueEditor::ValueViewModel::singlePageMode() const { return m_singlePageMode; } bool ValueEditor::ValueViewModel::isRowLoaded(int i) { if (!m_model) { qWarning() << "Model is not loaded"; return false; } return m_model->isRowLoaded(i); } void ValueEditor::ValueViewModel::loadRows(int start, int limit) { if (!m_model) { qWarning() << "Model is not loaded"; return; } int rowsLeft = totalRowCount() - start; int loaded = (rowsLeft > limit) ? limit : rowsLeft; // frame already loaded if (m_model->isRowLoaded(start) && m_model->isRowLoaded(start + loaded - 1)) { m_startFramePosition = start; m_lastLoadedRowFrameSize = loaded; emit layoutAboutToBeChanged(); emit rowsLoaded(start, loaded); emit layoutChanged(); return; } QString msg = QCoreApplication::translate("RESP", "Cannot load key value: %1"); m_model->loadRows( start, limit, [this, start, limit, msg](const QString& err, unsigned long rowsCount) { if (!err.isEmpty()) { emit error(msg.arg(err)); return; } m_lastLoadedRowFrameSize = rowsCount > limit ? limit : rowsCount; m_startFramePosition = start; emit layoutAboutToBeChanged(); emit rowsLoaded(start, m_lastLoadedRowFrameSize); emit layoutChanged(); }); } void ValueEditor::ValueViewModel::addRow(const QVariantMap& row) { if (!m_model) { qWarning() << "Model is not loaded"; return; } m_model->addRow(row, [this](const QString& err) { if (err.size() > 0) { emit error(err); return; } emit layoutChanged(); }); } void ValueEditor::ValueViewModel::updateRow(int rowIndex, const QVariantMap& row) { if (!m_model) { qWarning() << "Model is not loaded"; return; } if (rowIndex < 0 || !m_model->isRowLoaded(rowIndex)) return; m_model->updateRow(rowIndex, row, [this, rowIndex](const QString& err) { if (err.size() > 0) { emit error(err); return; } emit dataChanged(index(rowIndex, 0), index(rowIndex, m_model->getColumnNames().size() - 1)); emit valueUpdated(); }); } void ValueEditor::ValueViewModel::deleteRow(int rowIndex) { if (!m_model) { qWarning() << "Model is not loaded"; return; } if (rowIndex < 0 || !m_model->isRowLoaded(rowIndex)) return; m_model->removeRow(rowIndex, [this, rowIndex](const QString& err) { if (err.size() > 0) { emit error(err); return; } emit beginRemoveRows(QModelIndex(), rowIndex, rowIndex); emit endRemoveRows(); if (m_lastLoadedRowFrameSize > 0) m_lastLoadedRowFrameSize -= 1; if (m_model->rowsCount() == 0) emit keyRemoved(); }); } int ValueEditor::ValueViewModel::totalRowCount() { if (!m_model) { qWarning() << "Model is not loaded"; return 0; } return m_model->rowsCount(); } int ValueEditor::ValueViewModel::pageSize() { QSettings settings; return settings.value("app/valueEditorPageSize", 100).toInt(); } QVariantMap ValueEditor::ValueViewModel::getRow(int rowIndex) { if (!m_model) { qWarning() << "Model is not loaded"; return QVariantMap(); } if (rowIndex < 0 || !m_model->isRowLoaded(rowIndex)) return QVariantMap(); QHash names = roleNames(); QHashIterator i(names); QVariantMap res; while (i.hasNext()) { i.next(); if (i.value() == "display") continue; QVariant d = m_model->getData(rowIndex, i.key()); res[i.value()] = d; } return res; } void ValueEditor::ValueViewModel::loadRowsCount() { if (!m_model) { qWarning() << "Model is not loaded"; return; } m_model->loadRowsCount([this](const QString& err) { if (err.size() > 0) { emit error(err); return; } emit totalRowCountChanged(); }); } QVariant ValueEditor::ValueViewModel::filter(const QString& key) const { if (!m_model) { qWarning() << "Model is not loaded"; return QVariant(); } return m_model->filter(key); } void ValueEditor::ValueViewModel::setFilter(const QString& key, QVariant v) { if (!m_model) { qWarning() << "Model is not loaded"; return; } return m_model->setFilter(key, v); } ================================================ FILE: src/modules/value-editor/valueviewmodel.h ================================================ #pragma once #include #include #include #include #include "common/baselistmodel.h" #include "keymodel.h" namespace ValueEditor { class ValueViewModel : public BaseListModel { Q_OBJECT Q_PROPERTY(bool isLoaded READ isModelLoaded NOTIFY modelLoaded) Q_PROPERTY(bool singlePageMode READ singlePageMode WRITE setSinglePageMode NOTIFY singlePageModeChanged) Q_PROPERTY(int totalRowCount READ totalRowCount NOTIFY totalRowCountChanged) Q_PROPERTY(int pageSize READ pageSize NOTIFY pageSizeChanged) Q_PROPERTY( QVariantList columnNames READ columnNames NOTIFY columnNamesChanged) public: ValueViewModel(const QString& loadingTitle); ~ValueViewModel() override {} int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role) const override; QHash roleNames() const override; QSharedPointer model(); void setModel(QSharedPointer model); void setConnection(QSharedPointer c); QString tabLoadingTitle() const; void setTabError(const QString& t); void close(); public: // general key operations Q_INVOKABLE void renameKey(const QString& newKeyName); Q_INVOKABLE void setTTL(const QString& newTTL); Q_INVOKABLE void persistKey(); Q_INVOKABLE void removeKey(); // single row operations Q_INVOKABLE bool isRowLoaded(int i); Q_INVOKABLE void addRow(const QVariantMap& row); Q_INVOKABLE void updateRow(int i, const QVariantMap& row); Q_INVOKABLE void deleteRow(int i); Q_INVOKABLE QVariantMap getRow(int i); // multi row operations Q_INVOKABLE void loadRowsCount(); Q_INVOKABLE void loadRows(int start, int limit); Q_INVOKABLE void reload(); // filters Q_INVOKABLE QVariant filter(const QString& key) const; Q_INVOKABLE void setFilter(const QString&, QVariant); void setSinglePageMode(bool v); bool singlePageMode() const; bool isModelLoaded() const; int totalRowCount(); int pageSize(); QVariantList columnNames(); signals: void rowsLoaded(int start, int count); void error(QString error); void totalRowCountChanged(); void pageSizeChanged(); void columnNamesChanged(); void keyRenamed(); void keyRemoved(); void keyTTLChanged(); void singlePageModeChanged(); void modelLoaded(); void tabClosed(); void valueUpdated(); private: QSharedPointer m_model; QSharedPointer m_connection; int m_startFramePosition; int m_lastLoadedRowFrameSize; bool m_singlePageMode; QString m_tabTitle; }; } // namespace ValueEditor ================================================ FILE: src/py/formatters/__init__.py ================================================ from .binary import BinaryFormatter from .cbor import CBORFormatter from .msgpack import MsgpackFormatter from .phpserialize import PhpSerializeFormatter try: from .pickle import PickleFormatter pickle_formatter_loaded = True except Exception: pickle_formatter_loaded = False ENABLED_FORMATTERS = { "binary": BinaryFormatter(), "cbor": CBORFormatter(), "msgpack": MsgpackFormatter(), "php": PhpSerializeFormatter(), } # NOTE(u_glide): Numpy doesn't work on Windows 20.04 # For more info and progress on this issue see # https://github.com/numpy/numpy/issues/16744 if pickle_formatter_loaded: ENABLED_FORMATTERS["pickle"] = PickleFormatter() def get_formatters_list(): return [(name, f.read_only) for name, f in ENABLED_FORMATTERS.items()] def decode(name, value): formatter = ENABLED_FORMATTERS[name] error = "" read_only = formatter.read_only decode_format = formatter.decode_format try: result = formatter.decode(value) if type(result) is dict: result_dict = result result = result_dict.get('output', '') error = result_dict.get('error', error) read_only = result_dict.get('read-only', read_only) decode_format = result_dict.get('decode-format', decode_format) except Exception as e: read_only = True error = ( "Embedded formatter %s error: %s (value: %s)" % (name, str(e), value) ) result = "" return [error, result, read_only, decode_format] def validate(name, value): return ENABLED_FORMATTERS[name].validate(value) def encode(name, value): formatter = ENABLED_FORMATTERS[name] if formatter.read_only: return ["Formatter %s doesn't support encoding" % name] error = "" try: result = formatter.encode(value) except Exception as e: error = ( "Embedded formatter %s error: %s (value: %s)" % (name, str(e), value) ) result = "" return [error, result] ================================================ FILE: src/py/formatters/base.py ================================================ class BaseFormatter(object): read_only = True decode_format = "plain_text" def decode(self, value): raise NotImplementedError() def encode(self, value): raise NotImplementedError() def validate(self, value): try: result = self.decode(value) if type(result) is dict: err = result.get('error', '') return [err == '', err] else: return [True, ""] except Exception as e: return [False, str(e)] ================================================ FILE: src/py/formatters/binary.py ================================================ import bitstring from .base import BaseFormatter class BinaryFormatter(BaseFormatter): def decode(self, value): return bitstring.BitArray(value).bin ================================================ FILE: src/py/formatters/cbor.py ================================================ import cbor import json from .base import BaseFormatter class CBORFormatter(BaseFormatter): decode_format = "json" def decode(self, value): return json.dumps(cbor.loads(value), ensure_ascii=False) ================================================ FILE: src/py/formatters/msgpack.py ================================================ import base64 import io import json import msgpack from .base import BaseFormatter class MsgpackFormatter(BaseFormatter): read_only = False decode_format = "json" def decode(self, value): read_only = self.read_only unpacked = '' error = '' try: unpacked = msgpack.loads(value, raw=False, strict_map_key=False) except msgpack.ExtraData as e: read_only = True buf = io.BytesIO(value) unpacker = msgpack.Unpacker(buf, raw=False, strict_map_key=False) for data in unpacker: unpacked = data error = ('First object from the stream is shown, value was ' 'truncated by {extra_len} bytes.' .format(extra_len=len(e.extra))) break return { 'output': json.dumps(unpacked, default=self.default, ensure_ascii=False), 'read-only': read_only, 'error': error } def encode(self, value): return msgpack.dumps(json.loads(value)) @staticmethod def default(o): if isinstance(o, msgpack.Timestamp): return o.to_datetime().isoformat() elif isinstance(o, bytes): try: return o.decode("utf-8") except UnicodeDecodeError: return base64.b64encode(o) else: return str(o) ================================================ FILE: src/py/formatters/phpserialize.py ================================================ import phpserialize import json from .base import BaseFormatter class PhpSerializeFormatter(BaseFormatter): read_only = False decode_format = "json" def decode(self, value): read_only = self.read_only deserialized = '' error = '' try: deserialized = phpserialize.loads( value, decode_strings=True, object_hook=phpserialize.phpobject) except ValueError as e: read_only = True error = 'Value cannot be unserialized: {} (value: {})'.format( e, value) return { 'output': json.dumps(deserialized, ensure_ascii=False, default=self.default), 'read-only': read_only, 'error': error } def encode(self, value): return phpserialize.dumps(json.loads(value)) @staticmethod def default(o): if isinstance(o, phpserialize.phpobject): return o._asdict() ================================================ FILE: src/py/formatters/pickle.py ================================================ import json import pickle try: import numpy as np import pandas as pd numpy_support = True except ImportError: numpy_support = False from .base import BaseFormatter class PickleFormatter(BaseFormatter): decode_format = "json" def decode(self, value): def get_json_output(deserialized_object): return json.dumps(deserialized_object, default=self.default, ensure_ascii=False) read_only = self.read_only decode_format = self.decode_format deserialized = '' output = '' error = '' try: deserialized = pickle.loads(value) except pickle.UnpicklingError as e: read_only = True error = 'Value cannot be deserialized with pickle: {} ' \ '(value: {})'.format(e, value) if numpy_support: if isinstance(deserialized, pd.Series): output = f'{str(type(deserialized))[1:-1]}\n' \ f'{deserialized.to_string()}' decode_format = 'plain_text' elif isinstance(deserialized, pd.DataFrame): html = deserialized.to_html(render_links=True, border=0) output = self.format_html_output(deserialized, html) decode_format = 'html' elif isinstance(deserialized, np.ndarray): html = pd.DataFrame(deserialized).to_html(render_links=True, border=0) output = self.format_html_output(deserialized, html) decode_format = 'html' else: output = get_json_output(deserialized) else: output = get_json_output(deserialized) return { 'output': output, 'decode-format': decode_format, 'read-only': read_only, 'error': error } @staticmethod def default(o): if numpy_support: if isinstance(o, pd.Timestamp): return o.isoformat() if isinstance(o, np.ndarray): return o.tolist() if isinstance(o, pd.Series): return o.to_json() if isinstance(o, pd.DataFrame): return json.loads(o.to_json(orient='index', date_format='iso')) else: return str(o) else: return str(o) @staticmethod def format_html_output(data, html): style = '' return '{}

{}

{}'.format(style, str(type(data))[1:-1], html) ================================================ FILE: src/py/py.qrc ================================================ formatters/__init__.py formatters/base.py formatters/binary.py formatters/cbor.py formatters/msgpack.py formatters/pickle.py formatters/phpserialize.py rdb/__init__.py ================================================ FILE: src/py/rdb/__init__.py ================================================ import calendar import rdbtools from rdbtools.encodehelpers import STRING_ESCAPE_UTF8, STRING_ESCAPE_RAW VALID_TYPES = ("hash", "set", "string", "list", "sortedset") def process_command(callback, path_to_rdb, db, include_keys_pattern, exclude_keys_pattern, key_types, scan_keys=False): filters = {} if db: filters['dbs'] = [int(db)] if include_keys_pattern: filters['keys'] = include_keys_pattern if exclude_keys_pattern: filters['not_keys'] = exclude_keys_pattern if key_types: filters['types'] = [] for x in key_types: if not x in VALID_TYPES: raise ValueError( 'Invalid type provided - %s. ' 'Expected one of %s' % (x, (", ".join(VALID_TYPES))) ) else: filters['types'].append(x) parser = rdbtools.RdbParser(callback=callback, filters=filters, ignore_values=scan_keys) parser.parse(path_to_rdb) def rdb_list_keys(path_to_rdb, db, include_keys_pattern=None, exclude_keys_pattern=None, key_types=None): class KeysOnlyCallback(rdbtools.RdbCallback): def __init__(self, string_escape=None): super(KeysOnlyCallback, self).__init__(string_escape) self._out = set() def key(self, key): self._out.add(self.encode_key(key)) def keys(self): return list(self._out) callback = KeysOnlyCallback(string_escape=STRING_ESCAPE_UTF8) process_command(callback, path_to_rdb, db, include_keys_pattern, exclude_keys_pattern, key_types) return callback.keys() def rdb_export_as_commands(path_to_rdb, db, include_keys_pattern=None, exclude_keys_pattern=None, key_types=None): def _unix_timestamp(dt): return calendar.timegm(dt.utctimetuple()) class CommandsCallback(rdbtools.RdbCallback): def __init__(self, string_escape=None): super(CommandsCallback, self).__init__(string_escape) self._commands = [] self.reset() def reset(self): self._expires = {} def set_expiry(self, key, dt): self._expires[key] = dt def get_expiry_seconds(self, key): if key in self._expires: return _unix_timestamp(self._expires[key]) return None def expires(self, key): return key in self._expires def pre_expiry(self, key, expiry): if expiry is not None: self.set_expiry(key, expiry) def post_expiry(self, key): if self.expires(key): self.expireat(key, self.get_expiry_seconds(key)) def emit(self, *args): self._commands.append(args) def start_database(self, db_number): self.reset() self.select(db_number) # String handling def set(self, key, value, expiry, info): self.pre_expiry(key, expiry) self.emit(b'SET', key, value) self.post_expiry(key) # Hash handling def start_hash(self, key, length, expiry, info): self.pre_expiry(key, expiry) def hset(self, key, field, value): self.emit(b'HSET', key, field, value) def end_hash(self, key): self.post_expiry(key) # Set handling def start_set(self, key, cardinality, expiry, info): self.pre_expiry(key, expiry) def sadd(self, key, member): self.emit(b'SADD', key, member) def end_set(self, key): self.post_expiry(key) # List handling def start_list(self, key, expiry, info): self.pre_expiry(key, expiry) def rpush(self, key, value): self.emit(b'RPUSH', key, value) def end_list(self, key, info): self.post_expiry(key) # Sorted set handling def start_sorted_set(self, key, length, expiry, info): self.pre_expiry(key, expiry) def zadd(self, key, score, member): self.emit(b'ZADD', key, score, member) def end_sorted_set(self, key): self.post_expiry(key) # streams and modules, not currently supported def start_stream(self, key, listpacks_count, expiry, info): # TODO send RESTORE command pass def start_module(self, key, module_name, expiry, info): # TODO send RESTORE command return False # Other misc commands def select(self, db_number): self.emit(b'SELECT', db_number) def expireat(self, key, timestamp): self.emit(b'EXPIREAT', key, timestamp) def commands(self): return self._commands callback = CommandsCallback(string_escape=STRING_ESCAPE_RAW) process_command(callback, path_to_rdb, db, include_keys_pattern, exclude_keys_pattern, key_types) return callback.commands() ================================================ FILE: src/py/requirements.txt ================================================ bitstring cbor msgpack git+https://github.com/mrnom/phpserialize.git#egg=phpserialize git+https://github.com/uglide/redis-rdb-tools#egg=rdbtools python-lzf ================================================ FILE: src/qml/AppToolBar.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.15 import QtQuick.Controls.Styles 1.1 import Qt.labs.platform 1.1 import QtQml.Models 2.2 import "." import "./common" import "./common/platformutils.js" as PlatformUtils ToolBar { background: Rectangle { implicitHeight: 40 color: sysPalette.button } RowLayout { anchors.fill: parent spacing: 0 RowLayout { Layout.maximumWidth: connectionsTree.width + 1 BetterButton { Layout.fillWidth: true Layout.minimumWidth: 190 iconSource: PlatformUtils.getThemeIcon("add.svg") text: qsTranslate("RESP","Connect to Redis Server") objectName: "rdm_connect_to_redis_server_btn" onClicked: { connectionSettingsDialog.settings = connectionsManager.createEmptyConfig() connectionSettingsDialog.open() } } ImageButton { id: connectionsMenuBtn objectName: "rdm_connections_menu_btn" Layout.preferredWidth: 30 iconSource: PlatformUtils.getThemeIcon("list.svg") onClicked: menu.open() FileDialog { id: importConnectionsDialog title: qsTranslate("RESP","Import Connections") nameFilters: ["Connections (*.json)"] fileMode: FileDialog.OpenFile onAccepted: connectionsManager.importConnections(qmlUtils.getPathFromUrl(file)) } FileDialog { id: exportConnectionsDialog title: qsTranslate("RESP","Export Connections") nameFilters: ["Connections (*.json)"] fileMode: FileDialog.SaveFile onAccepted: connectionsManager.saveConnectionsConfigToFile(qmlUtils.getPathFromUrl(file)) } BetterMenu { id: menu BetterMenuItem { objectName: "rdm_import_connections_btn" text: qsTranslate("RESP","Import Connections") onTriggered: importConnectionsDialog.open() } BetterMenuItem { objectName: "rdm_export_connections_btn" text: qsTranslate("RESP","Export Connections") onTriggered: exportConnectionsDialog.open() } } } ImageButton { id: toggleTreeViewBtn Layout.preferredWidth: 30 iconSource: PlatformUtils.getThemeIcon("square-half.svg") imgWidth: 15 imgHeight: 15 onClicked: { connectionsTreeWrapper.visible = !connectionsTreeWrapper.visible } } } Rectangle { width: 1; color: sysPalette.mid; Layout.fillHeight: true;} Item { Layout.fillWidth: true } BetterButton { implicitWidth: 40 iconSource: PlatformUtils.getThemeIcon("alert.svg") tooltip: qsTranslate("RESP","Report issue") onClicked: Qt.openUrlExternally("https://github.com/uglide/RedisDesktopManager/issues") } BetterButton { implicitWidth: 40 iconSource: PlatformUtils.getThemeIcon("help.svg") tooltip: qsTranslate("RESP","Documentation") onClicked: Qt.openUrlExternally("http://docs.resp.app/en/latest/") } BetterButton { implicitWidth: 40 iconSource: PlatformUtils.getThemeIcon("telegram.svg") tooltip: qsTranslate("RESP","Join Telegram Chat") onClicked: Qt.openUrlExternally("https://t.me/RedisDesktopManager") } BetterButton { implicitWidth: 40 iconSource: PlatformUtils.getThemeIcon("twi.svg") tooltip: qsTranslate("RESP","Follow") onClicked: Qt.openUrlExternally("https://twitter.com/dev_rdm") } BetterButton { implicitWidth: 40 iconSource: PlatformUtils.getThemeIcon("github.svg") tooltip: qsTranslate("RESP","Star on GitHub!") onClicked: Qt.openUrlExternally("https://github.com/uglide/RedisDesktopManager") } Item { Layout.fillWidth: true } BetterButton { iconSource: PlatformUtils.getThemeIcon("log.svg") text: qsTranslate("RESP","Log") onClicked: logDrawer.open() } BetterButton { objectName: "rdm_extension_server_settings_btn" iconSource: PlatformUtils.getThemeIcon("server_2.svg") text: qsTranslate("RESP","Extension Server") onClicked: { extServerSettingsDialog.item.open() } } BetterButton { objectName: "rdm_global_settings_btn" iconSource: PlatformUtils.getThemeIcon("settings.svg") text: qsTranslate("RESP","Settings") onClicked: { settingsDialog.item.open() } } } } ================================================ FILE: src/qml/LogView.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.13 import "./common" FastTextView { id: root property alias eventsModel: modelConnections.target model: ListModel {} function dumpText() { var allStrings = ""; for (var ind=0; ind < root.model.count; ind++) { allStrings += root.model.get(ind)["msg"] + "\n" } return allStrings } color: sysPalette.base border.color: sysPalette.shadow border.width: 1 showLineNumbers: false Connections { id: modelConnections function onLog(msg) { if (model.count > 1500) { model.remove(0, model.count - 1000) } model.append({"msg": msg}) positionViewAtEnd() } } } ================================================ FILE: src/qml/QuickStartDialog.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.13 import QtQuick.Window 2.3 import "./common" import "./common/platformutils.js" as PlatformUtils BetterDialog { id: root objectName: "rdm_quick_start_dialog" title: qsTranslate("RESP","Getting Started") footer: null contentItem: Rectangle { id: rootItem color: sysPalette.base anchors.fill: parent implicitWidth: 750 implicitHeight: 150 Control { palette: approot.palette anchors.fill: parent anchors.margins: 30 ColumnLayout { anchors.fill: parent Item { Layout.fillHeight: true } RowLayout { id: msgLayout Layout.fillHeight: true Layout.alignment: Qt.AlignHCenter BetterLabel { Layout.fillWidth: true wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter text: qsTranslate("RESP","Thank you for choosing RESP.app. Let's make your Redis experience better.") font.pixelSize: 16 Component.onCompleted: { if (!PlatformUtils.isOSX()) { root.width = contentWidth + 100 } } } } Item { Layout.fillHeight: true } RowLayout { Item { Layout.fillWidth: true } BetterButton { text: qsTranslate("RESP","Connect to Redis-Server") palette.button: "#c6302b" palette.buttonText: "#ffffff" onClicked: { root.close() connectionSettingsDialog.settings = connectionsManager.createEmptyConfig() connectionSettingsDialog.open() } } BetterButton { property string url: "http://docs.resp.app/en/latest/quick-start/" text: qsTranslate("RESP","Read the Docs") tooltip: url onClicked: Qt.openUrlExternally(url) } Item { Layout.fillWidth: true } } Item { Layout.fillHeight: true } } } } } ================================================ FILE: src/qml/WelcomeTab.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 import "./common" import "./common/platformutils.js" as PlatformUtils BetterTab { id: root ColumnLayout { anchors.centerIn: parent width: Math.min(parent.width * 0.9, 600) RowLayout { id: topLayout spacing: 15 Layout.fillWidth: true Layout.preferredHeight: 350 Image { id: logo source: "qrc:/images/redisinsight.svg" Layout.preferredWidth: 50 Layout.preferredHeight: 50 Layout.alignment: Qt.AlignTop fillMode: Image.PreserveAspectFit } ColumnLayout { Layout.fillWidth: true RichTextWithLinks { Layout.fillWidth: true; html: 'RedisInsight is the successor to RESP.app'} RichTextWithLinks { Layout.fillWidth: true; html: '
In 2022, Redis joined forces with the creator of RESP.app, Igor Malinovskyi, ' + 'bringing RESP.app’s popular features into RedisInsight. RedisInsight now provides improved ' + 'performance and these additional features:
' + '
    ' + '
  • SSH tunneling support
  • ' + '
  • Support for RedisStack
  • ' + '
  • Database analysis and performance improvement recommendations
  • ' + '
  • Advanced CLI with syntax highlighting and autocomplete
  • ' + '
  • … and much, much more
  • ' + '
' } RowLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter Layout.margins: 20; BetterButton { text: qsTranslate("RESP","Download from Snapcraft") onClicked: Qt.openUrlExternally("https://snapcraft.io/redisinsight") visible: PlatformUtils.isLinux() } BetterButton { text: qsTranslate("RESP","Download from Flathub") onClicked: Qt.openUrlExternally("https://flathub.org/apps/details/com.redis.RedisInsight") visible: PlatformUtils.isLinux() } BetterButton { text: qsTranslate("RESP","Download from Microsoft Store") onClicked: Qt.openUrlExternally("https://apps.microsoft.com/store/detail/redisinsight/XP8K1GHCB0F1R2") visible: PlatformUtils.isWindows() } BetterButton { text: qsTranslate("RESP","Download from AppStore") onClicked: Qt.openUrlExternally("https://apps.apple.com/us/app/redisinsight/id6446987963") visible: PlatformUtils.isOSX() } BetterButton { text: PlatformUtils.isWindows() ? qsTranslate("RESP","Download Installer") : qsTranslate("RESP","Download DMG") onClicked: Qt.openUrlExternally("https://redis.com/redis-enterprise/redis-insight/") visible: !PlatformUtils.isLinux() } } RichTextWithLinks { Layout.fillWidth: true; html: '
Thank you for being a RESP.app user! ' + ' To transfer your connections to RedisInsight seamlessly, please follow the migration guide. If you face any migration issues, please contact igor@resp.app.
'} } } } } ================================================ FILE: src/qml/app.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.13 import QtQuick.Controls.Styles 1.1 import QtQml.Models 2.2 import QtQuick.Window 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.3 as LegacyDialogs import "." import "./common" import "./common/platformutils.js" as PlatformUtils import "./value-editor/" import "./value-editor/editors/formatters/" import "./connections" import "./connections-tree" import "./console" import "./server-actions" import "./bulk-operations" import "./settings" ApplicationWindow { id: approot visible: true objectName: "rdm_qml_root" title: "RESP.app - GUI for Redis® " + Qt.application.version width: 1180 height: 800 minimumWidth: 1000 minimumHeight: 600 property bool darkModeEnabled: sysPalette.base.hslLightness < 0.4 property double wRatio : (width * 1.0) / (Screen.width * 1.0) property double hRatio : (height * 1.0) / (Screen.height * 1.0) property var currentValueFormatter property var embeddedFormatters ValueFormatters { id: valueFormattersModel } Component.onCompleted: { if (hRatio > 1 || wRatio > 1) { console.log("Ratio > 1.0. Resize main window.") width = Screen.width * 0.9 height = Screen.height * 0.8 } if (Qt.platform.os == "windows") { x = Screen.width / 2 - width / 2 y = Screen.height / 2 - height / 2 } appSplitView.restoreState(windowSettings.splitView) } Component.onDestruction: windowSettings.splitView = appSplitView.saveState() Settings { id: windowSettings category: "windows_settings" property alias width: approot.width property alias height: approot.height property var splitView } Settings { id: appSettings category: "app" property string valueEditorFont property string valueEditorFontSize property int valueSizeLimit: 1500000 } Settings { id: defaultFormatterSettings category: "formatter_overrides" } Settings { id: defaultCompressionSettings category: "compression_overrides" } SystemPalette { id: sysPalette } SystemPalette { id: inactiveSysPalette colorGroup: SystemPalette.Inactive } SystemPalette { id: disabledSysPalette colorGroup: SystemPalette.Disabled } QuickStartDialog { id: quickStartDialog objectName: "rdm_qml_quick_start_dialog" width: PlatformUtils.isOSX() ? 600 : approot.width * 0.8 } Loader { id: settingsDialog asynchronous: true source: "settings/GlobalSettings.qml" } Loader { id: extServerSettingsDialog asynchronous: true source: "extension-server/ExtensionServerSettings.qml" } ConnectionSettignsDialog { id: connectionSettingsDialog objectName: "rdm_connection_settings_dialog" onTestConnection: { connectionsManager.testConnectionSettings(settings, connectionTested) } function connectionTested(result) { if (result) { hideLoader() showMsg(qsTranslate("RESP","Successful connection to redis-server")) } else { hideLoader() showError(qsTranslate("RESP","Can't connect to redis-server")) } } onSaveConnection: connectionsManager.updateConnection(settings) } AskSecretDialog { id: askSecretDialog } ConnectionGroupDialog { id: connectionGroupDialog objectName: "rdm_connection_group_dialog" onAddNewGroup: { connectionsManager.addNewGroup(name) } onEditGroup: { connectionsManager.updateGroup(group) } } Loader { id: notification property var icon property string text property string details function showError(msg, details="") { icon = LegacyDialogs.StandardIcon.Warning text = msg notification.details = details sourceComponent = notificationTemplate } function showMsg(msg) { icon = LegacyDialogs.StandardIcon.Information text = msg details = "" sourceComponent = notificationTemplate } onLoaded: { item.open() } Component { id: notificationTemplate OkDialog { objectName: "rdm_qml_error_dialog" visible: false icon: notification.icon text: notification.text detailedText: notification.details onVisibleChanged: { if (!visible) { notification.sourceComponent = undefined } } } } } AddKeyDialog { id: addNewKeyDialog } Connections { target: serverStatsModel ignoreUnknownSignals: true function onError(error) { notification.showError(error) } } Connections { target: keyFactory function onNewKeyDialog(r) { addNewKeyDialog.request = r addNewKeyDialog.open() } } BulkOperationsDialog { id: bulkOperationDialog } Connections { target: bulkOperations function onOpenDialog(operationName) { bulkOperationDialog.operationName = operationName bulkOperationDialog.open() } } Connections { target: appEvents function onError(msg) { notification.showError(msg) } function onPythonLoaded() { valueFormattersModel.loadEmbeddedFormatters(); valueFormattersModel.updateRWFormatters(); } function onExternalFormattersLoaded() { valueFormattersModel.loadExternalFormatters(); valueFormattersModel.updateRWFormatters(); } } Connections { target: connectionsManager function onEditConnection(config) { connectionSettingsDialog.settings = config connectionSettingsDialog.open() } function onEditConnectionGroup(group) { connectionGroupDialog.group = group connectionGroupDialog.open() } function onConnectionsLoaded() { if (connectionsManager.size() === 0) quickStartDialog.open() } function onAskUserForConnectionSecret(config, id) { console.log("Ask user for secret", config.name, id) askSecretDialog.secretId = id; askSecretDialog.config = config; askSecretDialog.open() askSecretDialog.forceFocus() } } header: AppToolBar {} Rectangle { id: appWrapper anchors.fill: parent color: sysPalette.base border.color: sysPalette.mid border.width: 1 BetterSplitView { id: appSplitView anchors.fill: parent anchors.topMargin: 1 orientation: Qt.Horizontal ColumnLayout { id: connectionsTreeWrapper SplitView.fillHeight: true SplitView.minimumWidth: 404 SplitView.minimumHeight: 500 BetterTreeView { id: connectionsTree Layout.fillHeight: true Layout.fillWidth: true } RowLayout { Layout.fillWidth: true Layout.margins: 10 BetterButton { id: addConnectionGroupBtn objectName: "rdm_add_group_btn" iconSource: PlatformUtils.getThemeIcon("add.svg") text: qsTranslate("RESP", "Add Group") Layout.fillWidth: true visible: sortButton.visible onClicked: { connectionGroupDialog.group = undefined connectionGroupDialog.open() } } BetterButton { id: sortButton objectName: "rdm_regroup_connections_btn" text: qsTranslate("RESP", "Regroup connections") iconSource: PlatformUtils.getThemeIcon("sort.svg") Layout.fillWidth: true onClicked: { connectionsTree.sortConnections = true connectionsTree.selection.clear() connectionsTree.backgroundVisible = true connectionsManager.collapseRootItems() sortButton.visible = false } } BetterButton { id: sortApplyButton objectName: "rdm_exit_regroup_mode_btn" Layout.fillWidth: true text: qsTranslate("RESP", "Exit Regroup Mode") visible: !sortButton.visible iconSource: PlatformUtils.getThemeIcon("ok.svg") onClicked: { connectionsTree.sortConnections = false connectionsTree.backgroundVisible = false connectionsManager.applyGroupChanges() sortButton.visible = true } } } } ColumnLayout { SplitView.fillWidth: true SplitView.fillHeight: true TabBar { id: tabBar objectName: "rdm_main_tab_bar" Layout.fillWidth: true Layout.preferredHeight: 30 background: Rectangle { color: sysPalette.base } onCountChanged: { updateTimer.start() } function activateTabButton(item) { for (var btnIndex in contentChildren) { if (contentChildren[btnIndex] == item) { currentIndex = btnIndex; break; } } } Timer { id: updateTimer interval: 50; running: false; repeat: false onTriggered: { if (tabBar.count > 0) { tabs.activateTab(tabBar.itemAt(tabBar.currentIndex).tabRef) if (tabBar.currentIndex == 0 ) { tabBar.currentIndex = -1 tabBar.currentIndex = 0 } } } } } StackLayout { id: tabs objectName: "rdm_qml_tabs" Layout.fillHeight: true Layout.fillWidth: true Layout.minimumWidth: 550 Layout.minimumHeight: 30 onCountChanged: { if (count === 1) { currentIndex = 0; } } function activateTab(item) { var realIndex = 0; for (var tIndex in tabs.children) { if (!tabs.children[tIndex].__isTab) { continue; } if (tabs.children[tIndex] === item) { tabs.currentIndex = realIndex; item.activate(); break; } realIndex++; } } WelcomeTab { id: welcomeTab clip: true objectName: "rdm_qml_welcome_tab" visible: tabs.count == 1 } ServerActionTabs { objectName: "rdm_qml_server_info_tabs" model: serverStatsModel } ValueTabs { objectName: "rdm_qml_value_tabs" model: valuesModel } Consoles { objectName: "rdm_qml_console_tabs" model: consoleModel } } Connections { target: valuesModel ignoreUnknownSignals: true function onTabError(index, error) { if (index != -1) tabs.currentIndex = index notification.showError(error) } } } } } Drawer { id: logDrawer dragMargin: 0 width: 0.66 * approot.width height: approot.height position: 0.3 edge: Qt.LeftEdge background: Rectangle { color: sysPalette.base border.color: sysPalette.mid } LogView { anchors.fill: parent eventsModel: appEvents } } } ================================================ FILE: src/qml/bulk-operations/BulkOperationsDialog.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.13 import QtQuick.Dialogs 1.3 import "./../common/" import "../common/platformutils.js" as PlatformUtils BetterDialog { id: root title: qsTranslate("RESP","Bulk Operations Manager") footer: null property string operationName: bulkOperations.operationName property int firstColSize: PlatformUtils.isScalingDisabled()? 300 : 250 standardButtons: StandardButton.NoButton function loadKeys() { bulkOperations.getAffectedKeys() } onVisibleChanged: { if (visible == false) { bulkOperations.clearOperation(); resetKeysPreview() } else { targetConnection.model = bulkOperations.getTargetConnections() } } function resetKeysPreview() { keysPreview.visible = false btnShowAffectedKeys.visible = true spacer.visible = true } function setMetadata() { bulkOperations.setOperationMetadata( { "ttl": ttlValue.value, "replace": replaceKeys.checked ? "replace": "", "path": rdbPath.path, "db": rdbDb.value } ) } function showError(title, text, details) { uiBlocker.visible = false bulkErrorNotification.title = title bulkErrorNotification.text = text if (details) { bulkErrorNotification.detailedText = details } else { bulkErrorNotification.detailedText = "" } bulkErrorNotification.open() } function validate() { if (root.operationName == "rdb_import" && !qmlUtils.fileExists(rdbPath.path)) { rdbPath.validationError = true showError(qsTranslate("RESP","Invalid RDB path"), qsTranslate("RESP","Please specify valid path to RDB file"), "") return false; } rdbPath.validationError = false return true; } contentItem: Rectangle { id: contentWrapper implicitWidth: PlatformUtils.isScalingDisabled() ? 1100 : 900 implicitHeight: PlatformUtils.isScalingDisabled() ? 650 : 600 color: sysPalette.base Control { palette: approot.palette anchors.fill: parent state: root.operationName states: [ State { name: "delete_keys" PropertyChanges { target: operationLabel; text: qsTranslate("RESP","Delete keys") } PropertyChanges { target: actionButton; text: qsTranslate("RESP","Delete keys") } PropertyChanges { target: ttlField; visible: false } PropertyChanges { target: replaceKeysField; visible: false } PropertyChanges { target: targetConnectionSettings; visible: false } PropertyChanges { target: rdbImportFields; visible: false; } }, State { name: "ttl" PropertyChanges { target: operationLabel; text: qsTranslate("RESP","Set TTL for multiple keys") } PropertyChanges { target: actionButton; text: qsTranslate("RESP","Set TTL") } PropertyChanges { target: ttlField; visible: true } PropertyChanges { target: replaceKeysField; visible: false } PropertyChanges { target: targetConnectionSettings; visible: false } PropertyChanges { target: rdbImportFields; visible: false; } }, State { name: "copy_keys" PropertyChanges { target: operationLabel; text: qsTranslate("RESP","Copy keys to another database") } PropertyChanges { target: actionButton; text: qsTranslate("RESP","Copy keys") } PropertyChanges { target: ttlField; visible: true } PropertyChanges { target: replaceKeysField; visible: true } PropertyChanges { target: targetConnectionSettings; visible: true } PropertyChanges { target: rdbImportFields; visible: false; } }, State { name: "rdb_import" PropertyChanges { target: operationLabel; text: qsTranslate("RESP","Import data from rdb file") } PropertyChanges { target: actionButton; text: qsTranslate("RESP","Import") } PropertyChanges { target: ttlField; visible: false } PropertyChanges { target: replaceKeysField; visible: false } PropertyChanges { target: targetConnectionSettings; visible: false } PropertyChanges { target: rdbImportFields; visible: true; } } ] ColumnLayout { anchors.fill: parent anchors.margins: 20 BetterLabel { id: operationLabel font.pixelSize: 20 } Rectangle { color: sysPalette.mid Layout.preferredHeight: 1 Layout.fillWidth: true } Item { Layout.preferredHeight: 5 } GridLayout { id: sourceConnectionSettings columns: 2 Layout.fillWidth: true BetterLabel { text: qsTranslate("RESP","Redis Server:") Layout.preferredWidth: root.firstColSize Layout.preferredHeight: 25 } BetterLabel { Layout.fillWidth: true Layout.preferredHeight: 25 text: bulkOperations.connectionName } BetterLabel { text: qsTranslate("RESP","Database number:") Layout.preferredWidth: root.firstColSize Layout.preferredHeight: 25 } BetterLabel { Layout.fillWidth: true Layout.preferredHeight: 25 text: bulkOperations.dbIndex } GridLayout { id: rdbImportFields columns: 2 Layout.rowSpan: 2 Layout.columnSpan: 2 Layout.fillWidth: true BetterLabel { id: rdbPathLabel text: qsTranslate("RESP","Path to RDB file:") Layout.preferredWidth: root.firstColSize } FilePathInput { id: rdbPath Layout.fillWidth: true objectName: "rdm_bulk_operations_dialog_rdb_path" placeholderText: qsTranslate("RESP","Path to dump.rdb file") nameFilters: [ "RDB (*.rdb)" ] title: qsTranslate("RESP","Select dump.rdb") path: "" onPathChanged: { console.log(rdbPath.path) } } BetterLabel { id: rdbDbLabel text: qsTranslate("RESP","Select DB in RDB file:") Layout.preferredWidth: root.firstColSize } BetterSpinBox { id: rdbDb Layout.fillWidth: true from: 0 to: 10000000 value: 0 onValueChanged: { setMetadata() root.resetKeysPreview() } } } BetterLabel { text: root.operationName == "rdb_import"? qsTranslate("RESP","Import keys that match regex:") : qsTranslate("RESP","Key pattern:") Layout.preferredWidth: root.firstColSize } BetterTextField { objectName: "rdm_bulk_operations_dialog_key_pattern" Layout.fillWidth: true text: bulkOperations.keyPattern onTextChanged: { bulkOperations.keyPattern = text root.resetKeysPreview() } } RowLayout { id: ttlField Layout.fillWidth: true Layout.columnSpan: 2 BetterLabel { text: "New TTL value (seconds):" Layout.preferredWidth: root.firstColSize } BetterSpinBox { id: ttlValue objectName: "rdm_bulk_operations_dialog_ttl_value" Layout.fillWidth: true from: -1 to: 10000000 value: 0 } } } GridLayout { id: targetConnectionSettings columns: 2 Layout.fillWidth: true visible: bulkOperations.multiConnectionOperation() BetterLabel { Layout.preferredWidth: root.firstColSize text: qsTranslate("RESP","Destination Redis Server:") } BetterComboBox { id: targetConnection objectName: "rdm_bulk_operations_dialog_connection_combobox" Layout.fillWidth: true } BetterLabel { Layout.preferredWidth: root.firstColSize text: qsTranslate("RESP","Destination Redis Server Database Index:") } BetterSpinBox { id: targetDatabaseIndex Layout.fillWidth: true objectName: "rdm_bulk_operations_dialog_target_db_index" from: 0 to: 10000000 value: 0 } RowLayout{ id: replaceKeysField Layout.columnSpan: 2 BetterLabel { text: "Replace existing keys in target db:" Layout.preferredWidth: root.firstColSize } BetterCheckbox { id: replaceKeys objectName: "rdm_bulk_operations_dialog_replace_keys" Layout.fillWidth: true } } } Item { Layout.preferredHeight: 10 } BetterButton { id: btnShowAffectedKeys text: root.operationName == "rdb_import"? qsTranslate("RESP","Show matched keys") : qsTranslate("RESP","Show Affected keys") onClicked: { if (!validate()) { return; } uiBlocker.visible = true setMetadata() root.loadKeys() btnShowAffectedKeys.visible = false spacer.visible = false keysPreview.visible = true } } ColumnLayout { id: keysPreview Layout.fillWidth: true Layout.fillHeight: true visible: false BetterLabel { text: root.operationName == "rdb_import"? qsTranslate("RESP","Matched keys:") : qsTranslate("RESP","Affected keys:") } FastTextView { id: affectedKeysListView color: sysPalette.base border.color: sysPalette.shadow border.width: 1 Layout.fillWidth: true Layout.fillHeight: true Connections { target: bulkOperations function onAffectedKeys(r) { console.log("Affected keys loaded") affectedKeysListView.model = r uiBlocker.visible = false } function onOperationFinished() { affectedKeysListView.model = [] uiBlocker.visible = false bulkSuccessNotification.text = qsTranslate("RESP","Bulk Operation finished.") bulkSuccessNotification.open() } function onError(e, details) { showError(qsTranslate("RESP","Bulk Operation finished with errors"), e, details) } } } } Item { id: spacer; Layout.fillHeight: true } RowLayout { Layout.fillWidth: true Item { Layout.fillWidth: true; } BetterButton { id: actionButton objectName: "rdm_bulk_operations_dialog_action_button" onClicked: { if (!validate()) { return; } setMetadata() bulkConfirmation.open() } } BetterButton { text: qsTranslate("RESP","Cancel") onClicked: root.close() } } } Rectangle { id: uiBlocker visible: false anchors.fill: parent color: Qt.rgba(0, 0, 0, 0.1) Item { anchors.fill: parent ColumnLayout { anchors.centerIn: parent; BusyIndicator { running: true } BetterLabel { text: { if (bulkOperations.operationProgress > 0) return qsTranslate("RESP","Processed: ") + bulkOperations.operationProgress else { return qsTranslate("RESP", "Getting list of affected keys...") } } } } } MouseArea { anchors.fill: parent } } Loader { id: bulkErrorNotification property var icon: StandardIcon.Warning property string title property string text property string detailedText Component { id: bulkNotificationTemplate OkDialog { modality: Qt.NonModal title: bulkErrorNotification.title icon: bulkErrorNotification.icon text: bulkErrorNotification.text detailedText: bulkErrorNotification.detailedText onVisibleChanged: { if (!visible) { bulkErrorNotification.sourceComponent = undefined } } } } onLoaded: { item.open() } function open() { sourceComponent = bulkNotificationTemplate } } OkDialogOverlay { id: bulkSuccessNotification title: qsTranslate("RESP","Success") x: (root.width - width) / 2 y: (root.height - height) / 3 visible: false onAccepted: cleanUp() onVisibleChanged: { if (visible == false) cleanUp() } function cleanUp() { bulkOperations.clearOperation(); uiBlocker.visible = false root.close() } } BetterMessageDialog { id: bulkConfirmation x: (root.width - width) / 2 y: (root.height - height) / 3 title: qsTranslate("RESP", "Confirmation") text: qsTranslate("RESP", "Do you really want to perform bulk operation?") onYesClicked: { uiBlocker.visible = true bulkOperations.runOperation(targetConnection.currentIndex, targetDatabaseIndex.value) } visible: false } } } } ================================================ FILE: src/qml/common/AddressInput.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.3 import "." RowLayout { property alias placeholderText: textField.placeholderText property alias host: textField.text property alias port: portField.value property alias validationError: textField.validationError BetterTextField { id: textField objectName: "rdm_connection_address_host_field" Layout.fillWidth: true } BetterLabel { text: ":" } BetterSpinBox { id: portField objectName: "rdm_connection_address_port_field" from: 1 to: 10000000 value: 22 } } ================================================ FILE: src/qml/common/BetterButton.qml ================================================ import QtQuick 2.9 import QtQuick.Controls 2.3 Button { id: root property string iconSource property string tooltip icon.source: root.iconSource icon.width: 18 icon.height: 18 icon.color: "transparent" implicitHeight: 30 opacity: root.enabled ? 1.0 : 0.8 font.capitalization: Font.Capitalize palette.button: sysPalette.button palette.brightText: sysPalette.highlightedText palette.windowText: sysPalette.text palette.buttonText: enabled ? sysPalette.text : disabledSysPalette.text palette.highlight: sysPalette.highlight MouseArea { id: mouseArea anchors.fill: parent onPressed: mouse.accepted = false cursorShape: Qt.PointingHandCursor } BetterToolTip { title: root.tooltip } } ================================================ FILE: src/qml/common/BetterCheckbox.qml ================================================ import QtQuick 2.9 import QtQuick.Controls 2.3 CheckBox { id: checkBox palette.windowText: sysPalette.windowText indicator: Rectangle { implicitWidth: 16 implicitHeight: 16 x: checkBox.leftPadding y: parent.height / 2 - height / 2 radius: 3 color: checkBox.down || !checkBox.enabled ? sysPalette.dark : sysPalette.base border.color: sysPalette.dark Rectangle { width: 10 height: 10 x: 3 y: 3 radius: 2 color: checkBox.down ? sysPalette.dark : sysPalette.text visible: checkBox.checked } } } ================================================ FILE: src/qml/common/BetterComboBox.qml ================================================ import QtQuick 2.13 import QtQuick.Controls 2.13 ComboBox { id: root implicitHeight: 30 palette.base: sysPalette.base palette.button: sysPalette.button palette.text: sysPalette.text palette.buttonText: sysPalette.text palette.highlightedText: sysPalette.buttonText palette.highlight: sysPalette.highlight palette.mid: sysPalette.mid palette.dark: sysPalette.dark palette.window: sysPalette.window function selectItem(txt) { var res = _select(txt); if (res >= 0) { activated(res) } } function _select(txt) { var index = find(txt) console.log("Index:", index) if (index !== -1) { currentIndex = index; } return index } } ================================================ FILE: src/qml/common/BetterDialog.qml ================================================ import QtQuick 2.13 import QtQuick.Controls 2.13 import "." Dialog { id: root x: (approot.width - width) / 2 y: (approot.height - height) / 3 parent: Overlay.overlay modal: true onRejected: { root.close() } background: Rectangle { color: sysPalette.base border.color: sysPalette.mid } header: BetterLabel { text: root.title visible: root.title elide: Label.ElideRight font.bold: true padding: 12 background: Rectangle { x: 1; y: 1 width: parent.width - 2 height: parent.height - 1 color: sysPalette.window } } footer: BetterDialogButtonBox { BetterButton { text: qsTranslate("RESP","Save") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole } BetterButton { text: qsTranslate("RESP","Cancel") onClicked: root.close() } } } ================================================ FILE: src/qml/common/BetterDialogButtonBox.qml ================================================ import QtQuick 2.13 import QtQuick.Controls 2.13 DialogButtonBox { spacing: 3 background: Rectangle { implicitHeight: 40 x: 1; y: 1 width: parent.width - 2 height: parent.height - 2 color: sysPalette.base } } ================================================ FILE: src/qml/common/BetterGroupbox.qml ================================================ import QtQuick.Controls 2.3 GroupBox { id: root property string labelText property alias checked: checkBox.checked palette.windowText: sysPalette.windowText palette.mid: sysPalette.mid spacing: 1 padding: 1 label: BetterCheckbox { id: checkBox objectName: "checkbox" text: root.labelText } } ================================================ FILE: src/qml/common/BetterLabel.qml ================================================ import QtQuick 2.9 import QtQuick.Controls 2.3 Label { color: sysPalette.text } ================================================ FILE: src/qml/common/BetterMenu.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 2.15 Menu { background: Rectangle { implicitWidth: 220 implicitHeight: 40 color: sysPalette.button border.color: sysPalette.mid radius: 2 } } ================================================ FILE: src/qml/common/BetterMenuItem.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 2.15 MenuItem { palette.windowText: sysPalette.text palette.midlight: sysPalette.midlight palette.light: sysPalette.light } ================================================ FILE: src/qml/common/BetterMessageDialog.qml ================================================ import QtQuick 2.13 import QtQuick.Controls 2.13 import "." BetterDialog { id: root implicitWidth: label.contentWidth + 50 property alias text: label.text BetterLabel { id: label anchors.fill: parent anchors.margins: 10 } signal yesClicked footer: BetterDialogButtonBox { spacing: 3 BetterButton { text: qsTranslate("RESP","Yes") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole onClicked: { root.yesClicked() } } BetterButton { text: qsTranslate("RESP","No") onClicked: root.close() } } } ================================================ FILE: src/qml/common/BetterRadioButton.qml ================================================ import QtQuick 2.9 import QtQuick.Controls 2.3 RadioButton { id: root property bool allowUncheck: false property bool _uncheck: false onPressed: { if (allowUncheck && checked) { _uncheck = true } } palette.windowText: sysPalette.windowText onReleased: { if (_uncheck) { checked = false _uncheck = false } } indicator: Rectangle { implicitWidth: 16 implicitHeight: 16 x: root.leftPadding y: parent.height / 2 - height / 2 radius: 8 color: root.down ? sysPalette.dark : sysPalette.base border.color: sysPalette.dark Rectangle { width: 10 height: 10 x: 3 y: 3 radius: 5 color: root.down ? sysPalette.dark : sysPalette.text visible: root.checked } } } ================================================ FILE: src/qml/common/BetterSpinBox.qml ================================================ import QtQuick 2.13 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.13 SpinBox { id: control implicitHeight: 30 editable: true textFromValue: renderText function renderText(value, locale) { return value } palette.text: sysPalette.text palette.highlight: sysPalette.highlight palette.highlightedText: sysPalette.highlightedText palette.button : sysPalette.button palette.base : sysPalette.base palette.mid: disabledSysPalette.highlight palette.buttonText: sysPalette.dark contentItem: TextInput { id: spinBoxTextInput z: 2 text: control.displayText font: control.font color: control.palette.text selectionColor: control.palette.highlight selectedTextColor: control.palette.highlightedText horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter readOnly: !control.editable selectByMouse: control.editable validator: control.validator inputMethodHints: control.inputMethodHints Rectangle { x: -6 - (control.down.indicator ? 1 : 0) y: -6 width: control.width - (control.up.indicator ? control.up.indicator.width - 1 : 0) - (control.down.indicator ? control.down.indicator.width - 1 : 0) height: control.height visible: control.activeFocus color: "transparent" border.color: control.palette.highlight border.width: 2 } } onFocusChanged: { if (focus == true) { spinBoxTextInput.selectAll() } } } ================================================ FILE: src/qml/common/BetterSplitView.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 2.13 SplitView { handle: Rectangle { implicitWidth: parent.orientation == Qt.Horizontal ? 3 : parent.width implicitHeight: parent.orientation == Qt.Vertical ? 3 : parent.height border.color: sysPalette.midlight border.width: 1 color: sysPalette.mid } } ================================================ FILE: src/qml/common/BetterTab.qml ================================================ import QtQuick 2.3 import QtQuick.Controls 2.3 Item { property bool __isTab: true signal activate() } ================================================ FILE: src/qml/common/BetterTabButton.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 2.13 TabButton { id: root implicitHeight: 30 leftPadding: 5 spacing: 0 icon.width: 18 icon.height: 18 icon.color: "transparent" property var self property var tabRef property string tooltip signal closeClicked() onClicked: { tabs.activateTab(tabRef) } palette.brightText: sysPalette.text palette.dark: sysPalette.button palette.mid: sysPalette.window palette.window: sysPalette.base palette.windowText: sysPalette.windowText ImageButton { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.rightMargin: { var spacing = (parent.width - parent.contentItem.implicitWidth) / 2 - 20; return Math.max(spacing, 0) } onClicked: { root.closeClicked() } } BetterToolTip { title: tooltip } } ================================================ FILE: src/qml/common/BetterTabView.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.1 import "." import "./platformutils.js" as PlatformUtils TabView { id: root style: TabViewStyle { tab: Rectangle { color: "#cccccc" implicitWidth: layout.implicitWidth + 3 implicitHeight: 30 radius: 3 Rectangle { id: content color: styleData.selected ? "white" : "#e2e2e2" anchors.fill: parent anchors.topMargin: control.tabPosition == Qt.TopEdge ? 1 : 0 anchors.rightMargin: 1 anchors.leftMargin: 1 anchors.bottomMargin: control.tabPosition == Qt.BottomEdge ? 1 : 0 RowLayout { id: layout anchors.fill: parent anchors.rightMargin: 8 Item { Layout.preferredWidth: 3 } AnimatedImage { source: { var icon = root.getTab(styleData.index) !== undefined ? root.getTab(styleData.index).icon : "" if (icon && icon.indexOf(".gif") > -1) { visible = true return icon } else { visible = false return "" } } width: 20 height: 20 } Image { source: { var icon = root.getTab(styleData.index) !== undefined ? root.getTab(styleData.index).icon : "" if (icon && icon.indexOf(".gif") == -1) { visible = true return icon } else { visible = false return "" } } sourceSize.width: 20 sourceSize.height: 20 } Text { color: sysPalette.text Layout.fillWidth: true Layout.minimumWidth: implicitWidth text: styleData.title } Item { visible: root.getTab(styleData.index) !== undefined && !root.getTab(styleData.index).closable Layout.preferredWidth: 3 } ImageButton { visible: root.getTab(styleData.index) !== undefined && root.getTab(styleData.index).closable Layout.preferredWidth: 18 Layout.preferredHeight: 18 imgSource: PlatformUtils.getThemeIcon("clear.svg") onClicked: root.getTab(styleData.index).closeTab(styleData.index) } } } } frame: Rectangle { color: "#e2e2e2" Rectangle { color: "white" anchors.fill: parent anchors.bottomMargin: 1 anchors.leftMargin: 1 anchors.rightMargin: 1 anchors.topMargin: 1 } } leftCorner: Item {} rightCorner: Item {} } } ================================================ FILE: src/qml/common/BetterTextField.qml ================================================ import QtQuick 2.9 import QtQuick.Controls 2.3 TextField { id: control property bool validationError: false property int bgImplicitWidth: 200 property int bgImplicitHeight: 30 property string tooltip selectByMouse: true color: sysPalette.text selectionColor: sysPalette.highlight selectedTextColor: sysPalette.highlightedText background: Rectangle { implicitWidth: control.bgImplicitWidth implicitHeight: control.bgImplicitHeight color: control.enabled? sysPalette.button : inactiveSysPalette.button border.width: control.validationError? 2 : 1 border.color: { if (control.validationError || !control.acceptableInput) return "#d12f24" return control.activeFocus ? sysPalette.highlight : sysPalette.mid } } BetterToolTip { title: control.tooltip } } ================================================ FILE: src/qml/common/BetterToolTip.qml ================================================ import QtQuick 2.9 import QtQuick.Controls 2.3 ToolTip { property string title visible: title && hovered contentItem: Text { text: title color: sysPalette.text } background: Rectangle { border.width: 1 color: sysPalette.base border.color: sysPalette.mid } } ================================================ FILE: src/qml/common/ColorInput.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.3 import QtQuick.Dialogs 1.3 import "./platformutils.js" as PlatformUtils RowLayout { id: root property alias placeholderText: textField.placeholderText property alias color: textField.text property alias title: dialog.title property alias validationError: textField.validationError function reset() { color = "" dialog.color = "" } Rectangle { implicitWidth: 30 implicitHeight: 30 color: textField.text? textField.text: "transparent" border.color: sysPalette.highlight border.width: 1 MouseArea { anchors.fill: parent onClicked: dialog.open() } } BetterTextField { id: textField objectName: root.objectName? root.objectName + "_text" : "" Layout.fillWidth: true } BetterButton { implicitHeight: 30 objectName: root.objectName? root.objectName + "_button" : "" text: qsTranslate("RESP","Select") onClicked: dialog.open() } ColorDialog { id: dialog onAccepted: textField.text = dialog.color } } ================================================ FILE: src/qml/common/FastTextView.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 2.13 import "./platformutils.js" as PlatformUtils Rectangle { id: root property alias model: listView.model property alias delegate: listView.delegate property bool showLineNumbers: true function positionViewAtEnd() { listView.positionViewAtEnd() } function dumpText() { var allStrings = ""; for (var id in root.model) { allStrings += root.model[id] + "\n" } return allStrings } ScrollView { anchors.fill: parent anchors.margins: 10 ScrollBar.vertical.policy: ScrollBar.AlwaysOn ListView { id: listView width: root.width - 20 delegate: TextEdit { color: sysPalette.text width: listView.width readOnly: true selectByMouse: true text: { if (root.showLineNumbers) { return (index+1) + ". " + modelData } else { return modelData } } wrapMode: Text.WrapAnywhere } } } MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: { menu.x = mouseX menu.y = mouseY menu.open() } } Menu { id: menu z: 255 MenuItem { text: "Copy" icon.source: PlatformUtils.getThemeIcon("copy.svg") icon.color: "transparent" onTriggered: { qmlUtils.copyToClipboard(root.dumpText()) } } } } ================================================ FILE: src/qml/common/FilePathInput.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.3 import Qt.labs.platform 1.1 import "./platformutils.js" as PlatformUtils RowLayout { id: root property alias placeholderText: textField.placeholderText property alias path: textField.text property alias nameFilters: fileDialog.nameFilters property alias title: fileDialog.title property alias validationError: textField.validationError BetterTextField { id: textField objectName: root.objectName? root.objectName + "_text" : "" readOnly: PlatformUtils.isOSX() Layout.fillWidth: true } BetterButton { implicitHeight: 30 objectName: root.objectName? root.objectName + "_button" : "" text: qsTranslate("RESP","Select File") onClicked: fileDialog.open() } FileDialog { id: fileDialog fileMode: FileDialog.OpenFile onAccepted: textField.text = qmlUtils.getPathFromUrl(fileDialog.file) } } ================================================ FILE: src/qml/common/ImageButton.qml ================================================ import QtQuick 2.9 import QtQuick.Controls 2.3 import "./platformutils.js" as PlatformUtils BetterButton { id: root implicitWidth: 18 implicitHeight: 18 property alias imgWidth: img.width property alias imgHeight: img.height property alias imgSource: img.source property alias iconSource: img.source property bool showBorder: false property bool imgStickTop: false MouseArea { id: mouseArea anchors.fill: parent onPressed: mouse.accepted = false cursorShape: Qt.PointingHandCursor } Image { id: img anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: root.text || root.imgStickTop ? null : parent.verticalCenter source: PlatformUtils.getThemeIcon("clear.svg") width: 18 height: 18 sourceSize.width: width * 2 sourceSize.height: height * 2 opacity: root.enabled? 1.0: 0.8 } contentItem: Text { text: root.text font: root.font opacity: enabled ? 1.0 : 0.8 color: root.down ? sysPalette.highlightedText : enabled ? sysPalette.text : disabledSysPalette.text horizontalAlignment: Text.AlignHCenter verticalAlignment: root.imgSource != "" ? Text.AlignBottom : Text.AlignVCenter elide: Text.ElideRight } background: Rectangle { implicitWidth: root.implicitWidth + 3 implicitHeight: root.implicitHeight + 3 opacity: root.enabled ? 1 : 0.3 color: root.hovered ? sysPalette.highlight : "transparent" border.width: root.hovered ? 1 : root.showBorder ? 1 : 0 border.color: root.hovered? sysPalette.highlight : sysPalette.mid radius: 5 } } ================================================ FILE: src/qml/common/JsonHighlighter.qml ================================================ import QtQuick 2.0 import rdm.models 1.0 Item { property alias textDocument: syntaxHighlighter.textDocument property bool _darkPalette: sysPalette.base.hslLightness < 0.4 TextCharFormat { id: keyFormat; foreground: _darkPalette? '#fcfcfc': '#000000' font.family: appSettings.valueEditorFont font.pointSize: appSettings.valueEditorFontSize } TextCharFormat { id: numberFormat; foreground: _darkPalette? '#008cff': '#0000ff' font.family: appSettings.valueEditorFont font.pointSize: appSettings.valueEditorFontSize } TextCharFormat { id: boolFormat; foreground: _darkPalette? '#d62929': '#b22222' font.family: appSettings.valueEditorFont font.pointSize: appSettings.valueEditorFontSize } TextCharFormat { id: stringFormat; foreground: _darkPalette? '#05a605': '#008000' font.family: appSettings.valueEditorFont font.pointSize: appSettings.valueEditorFontSize } TextCharFormat { id: nullFormat; foreground: _darkPalette? '#a8a8a8' : '#808080' font.family: appSettings.valueEditorFont font.pointSize: appSettings.valueEditorFontSize } SyntaxHighlighter { id: syntaxHighlighter onHighlightBlock: { let rx = /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g let m while ( ( m = rx.exec(text) ) !== null ) { var type = 'number'; if (/^"/.test(m[0])) { if (/:$/.test(m[0])) { setFormat(m.index, m[0].length, keyFormat); continue; } else { setFormat(m.index, m[0].length, stringFormat); continue; } } else if (/true|false/.test(m[0])) { setFormat(m.index, m[0].length, boolFormat); continue; } else if (/null/.test(m[0])) { setFormat(m.index, m[0].length, nullFormat); continue; } setFormat(m.index, m[0].length, numberFormat); continue; } } } } ================================================ FILE: src/qml/common/LegacyTableView.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 1.4 as LC import "./platformutils.js" as PlatformUtils LC.TableView { rowDelegate: Rectangle { height: 30 color: styleData.selected ? sysPalette.highlight : styleData.alternate? sysPalette.alternateBase : "transparent" } } ================================================ FILE: src/qml/common/NewTextArea.qml ================================================ import QtQuick 2.7 import "./platformutils.js" as PlatformUtils import "." TextEdit { id: root color: sysPalette.text wrapMode: TextEdit.WrapAnywhere font { family: appSettings.valueEditorFont pointSize: appSettings.valueEditorFontSize } selectByMouse: true property bool highlightJSON: false Loader { source: root.highlightJSON? "./JsonHighlighter.qml" : "" onLoaded: { if (item) { item.textDocument = root.textDocument } } } } ================================================ FILE: src/qml/common/OkDialog.qml ================================================ import QtQuick 2.0 import QtQuick.Dialogs 1.3 MessageDialog { id: root standardButtons: StandardButton.Ok } ================================================ FILE: src/qml/common/OkDialogOverlay.qml ================================================ import QtQuick 2.13 import QtQuick.Controls 2.13 import "." BetterDialog { id: root implicitWidth: label.contentWidth + 50 property alias text: label.text BetterLabel { id: label anchors.fill: parent anchors.margins: 10 } footer: BetterDialogButtonBox { BetterButton { text: qsTranslate("RESP","OK") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole } } } ================================================ FILE: src/qml/common/PasswordInput.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.3 RowLayout { id: root property alias placeholderText: textField.placeholderText property alias text: textField.text property alias validationError: textField.validationError property alias checkboxWidth: passwordMask.width signal accepted function forceFocus() { textField.forceActiveFocus() } BetterTextField { id: textField Layout.fillWidth: true echoMode: passwordMask.checked ? TextInput.Normal : TextInput.Password onAccepted: root.accepted() } BetterCheckbox { id: passwordMask text: qsTranslate("RESP","Show password") } } ================================================ FILE: src/qml/common/RichTextWithLinks.qml ================================================ import QtQuick 2.0 Text { property string html property string styleString: '' textFormat: Qt.RichText text: styleString + html wrapMode: Text.Wrap onLinkActivated: Qt.openUrlExternally(link) color: sysPalette.windowText MouseArea { anchors.fill: parent cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor acceptedButtons: Qt.NoButton } } ================================================ FILE: src/qml/common/SaveToFileButton.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 2.13 import Qt.labs.platform 1.1 import QtQuick.Layouts 1.1 import "./platformutils.js" as PlatformUtils ImageButton { id: root iconSource: raw ? PlatformUtils.getThemeIcon("binary_file.svg") : PlatformUtils.getThemeIcon("code_file.svg") tooltip: raw ? qsTranslate("RESP","Save Raw Value to File") : qsTranslate("RESP","Save Formatted Value to File") + " (" + shortcutText + ")" property string fileUrl property string folderUrl property string path property string shortcutText: "" property bool raw: false onClicked: saveToFile() function saveToFile() { saveValueToFileDialog.open() } FileDialog { id: saveValueToFileDialog title: raw ? qsTranslate("RESP","Save Raw Value") : qsTranslate("RESP","Save Formatted Value") nameFilters: ["All files (*)"] fileMode: FileDialog.SaveFile onAccepted: { root.fileUrl = file var path = qmlUtils.getPathFromUrl(file) root.folderUrl = qmlUtils.getUrlFromPath(qmlUtils.getDir(path)) root.path = qmlUtils.getNativePath(path) if (raw) { if (qmlUtils.saveToFile(value, root.path)) { saveToFileConfirmation.open() } } else { if (qmlUtils.saveToFile(textView.model.getText(), root.path)) { saveToFileConfirmation.open() } } } } BetterDialog { id: saveToFileConfirmation title: qsTranslate("RESP","Value was saved to file:") visible: false footer: null Rectangle { objectName: "rdm_save_to_file_confirmation_dialog" color: sysPalette.base anchors.fill: parent implicitWidth: 500 implicitHeight: 150 Control { palette: approot.palette anchors.fill: parent anchors.margins: 15 ColumnLayout { anchors.fill: parent TextEdit { objectName: "rdm_save_to_file_confirmation_dialog_path" Layout.fillWidth: true text: root.path color: sysPalette.text readOnly: true selectByMouse: true wrapMode: Text.Wrap } ColumnLayout { Layout.fillWidth: true Control { RowLayout { Layout.fillHeight: true spacing: 15 RichTextWithLinks { Layout.fillWidth: true wrapMode: Text.NoWrap html: "Open File" } RichTextWithLinks { Layout.fillWidth: true wrapMode: Text.NoWrap html: "Open Folder" } } } } RowLayout { Layout.fillHeight: true Item { Layout.fillWidth: true; } BetterButton { objectName: "rdm_save_to_file_confirmation_dialog_ok_btn" text: qsTranslate("RESP","OK") onClicked: saveToFileConfirmation.close() } } } } } } } ================================================ FILE: src/qml/common/SettingsGroupTitle.qml ================================================ import QtQuick.Controls 2.2 BetterLabel { font.pixelSize: 15 font.bold: true } ================================================ FILE: src/qml/common/platformutils.js ================================================ var dateTimeFormat = "yyyy-MM-dd hh:mm:ss.zzz" function isScalingDisabled() { return (Qt.platform.os == "windows" || Qt.platform.os == "linux") && screen.devicePixelRatio < 2 } function isWindows() { return Qt.platform.os == "windows" } function isLinux() { return Qt.platform.os == "linux" } function isOSX() { return Qt.platform.os == "osx" } function isOSXRetina(screen) { return isOSX() && screen.devicePixelRatio> 1 } function getThemeIcon(icon) { if (sysPalette.base.hslLightness < 0.4) { return "qrc:/images/dark_theme/" + icon } else { return "qrc:/images/light_theme/" + icon } } ================================================ FILE: src/qml/connections/AskSecretDialog.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.13 import QtQuick.Dialogs 1.3 import QtQuick.Window 2.3 import "./../common" Dialog { id: root modality: Qt.ApplicationModal title: qsTranslate("RESP","Enter " + getSecretName() + " to connect to ") + config.name property string secretId: "" property var config function getSecretName() { if (secretId === "ssh_password") { return qsTranslate("RESP","SSH Passphrase") } else { return qsTranslate("RESP","Unknown") } } function forceFocus() { secretValue.forceFocus() } contentItem: Rectangle { color: sysPalette.base implicitHeight: 100 implicitWidth: 600 Control { palette: approot.palette anchors.fill: parent ColumnLayout { anchors.fill: parent anchors.margins: 5 RowLayout { Layout.fillWidth: true BetterLabel { text: qsTranslate("RESP","Passphrase") } PasswordInput { id: secretValue objectName: "rdm_secret_input" onTextChanged: root.config.sshPassword = text onAccepted: { submitSecretBtn.submit() } } } RowLayout { Layout.fillWidth: true Layout.minimumHeight: 40 Item { Layout.fillWidth: true } BetterButton { id: submitSecretBtn Layout.preferredWidth: secretValue.checkboxWidth objectName: "rdm_secret_continue_btn" text: qsTranslate("RESP","Continue") function submit() { if (!secretValue.text) { return; } root.close() connectionsManager.proceedWithConnectionSecret(root.config) } onClicked: submit() } BetterButton { Layout.preferredWidth: secretValue.checkboxWidth objectName: "rdm_secret_cancel_btn" text: qsTranslate("RESP","Cancel") onClicked: root.close() } } } } } } ================================================ FILE: src/qml/connections/ConnectionSettignsDialog.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.13 import QtQuick.Window 2.3 import "../common" import "../common/platformutils.js" as PlatformUtils BetterDialog { id: root title: isNewConnection ? qsTranslate("RESP","New Connection Settings") : qsTranslate("RESP","Edit Connection Settings") + " " + settings.name footer: null property bool isNewConnection: !settings || !settings.name property var settings property string quickStartGuideUrl: "http://docs.resp.app/en/latest/quick-start/" signal testConnection signal saveConnection(var settings) signal resetSettings property var items: [] property var sshItems: [] property var sslItems: [] property var sshEnabled property var sslEnabled function cleanStyle() { function clean(items_array) { for (var index=0; index < items_array.length; index++) if (items_array[index].validationError !== undefined) items_array[index].validationError = false } clean(items) clean(sshItems) clean(sslItems) validationWarning.visible = false } function validate() { cleanStyle() function checkItems(items_array) { var errors = 0 for (var index=0; index < items_array.length; index++) { var value = undefined if (items_array[index].text != undefined) { value = items_array[index].text } else if (items_array[index].host != undefined) { value = items_array[index].host } else if (items_array[index].path != undefined) { value = items_array[index].path } if (value != undefined && value.length == 0) { errors++ items_array[index].validationError = true } } return errors } var errors_count = checkItems(items) if (sshEnabled) errors_count += checkItems(sshItems) if (sslEnabled) errors_count += checkItems(sslItems) return errors_count == 0 } function hideLoader() { uiBlocker.visible = false } function showLoader() { uiBlocker.visible = true } function showMsg(msg) { dialog_notification.showMsg(msg) } function showError(err) { dialog_notification.showError(err) } function isConnectionStringValid(connectionString) { return connectionsManager.isRedisConnectionStringValid(connectionString) } function parseConnectionString(connectionString) { return connectionsManager.parseConfigFromRedisConnectionString(connectionString) } onVisibleChanged: { if (visible) { connectionSettingsTabBar.currentIndex = isNewConnection ? 0 : 1 if (isNewConnection) { connectionStringField.forceActiveFocus() } } } contentItem: Rectangle { color: sysPalette.base implicitWidth: PlatformUtils.isScalingDisabled() ? 900 : 650 implicitHeight: { if (screen.devicePixelRatio === 1) { return connectionSettingsTabBar.implicitHeight + sshSettingsGrid.implicitHeight + 350 } else { return 610 } } Control { palette: approot.palette anchors.fill: parent ColumnLayout { anchors.fill: parent anchors.margins: 10 TabBar { id: connectionSettingsTabBar Layout.fillWidth: true palette.brightText: sysPalette.text palette.dark: sysPalette.button palette.mid: sysPalette.window palette.window: sysPalette.base palette.windowText: sysPalette.windowText TabButton { id: connectionWizardTabBtn objectName: "rdm_connection_settings_dialog_wizard_tab" text: qsTranslate("RESP","How to connect") visible: isNewConnection width: visible ? undefined : 0 } TabButton { objectName: "rdm_connection_settings_dialog_basic_settings_tab" text: qsTranslate("RESP","Connection Settings") } TabButton { objectName: "rdm_connection_settings_dialog_advanced_settings_tab" text: qsTranslate("RESP","Advanced Settings") } } StackLayout { id: settingsTabs Layout.fillWidth: true Layout.fillHeight: true currentIndex: connectionSettingsTabBar.currentIndex BetterTab { id: wizardTab Layout.fillWidth: true Layout.fillHeight: true Layout.margins: 30 ColumnLayout { anchors.fill: parent anchors.margins: 5 spacing: 20 SettingsGroupTitle { Layout.fillWidth: true text: qsTranslate("RESP","Create connection from Redis URL") } RowLayout { Layout.fillWidth: true BetterTextField { id: connectionStringField objectName: "rdm_connection_settings_dialog_wizard_connection_string_field" Layout.fillWidth: true focus: connectionWizardTabBtn.visible placeholderText: "redis://localhost:6379" KeyNavigation.tab: importConnectionStringBtn.enabled? importConnectionStringBtn : skipToNextTabBtn onAccepted: { importConnectionStringBtn.clicked() } onTextEdited: validationError = false } BetterButton { id: importConnectionStringBtn objectName: "rdm_connection_settings_dialog_wizard_import_btn" text: qsTranslate("RESP","Import") enabled: !!connectionStringField.text KeyNavigation.tab: skipToNextTabBtn onClicked: { if (!isConnectionStringValid(connectionStringField.text)) { connectionStringField.validationError = true } else { connectionStringField.validationError = false root.settings = parseConnectionString(connectionStringField.text) connectionSettingsTabBar.currentIndex = 1 connectionName.forceActiveFocus() } } Keys.onEnterPressed: { importConnectionStringBtn.clicked() } Keys.onReturnPressed: { importConnectionStringBtn.clicked() } } } RichTextWithLinks { Layout.fillWidth: true text: qsTranslate("RESP", "Learn more about Redis URL: ") + "redis://, " + "rediss://" } SettingsGroupTitle { Layout.fillWidth: true text: qsTranslate("RESP","Connection guides") } GridLayout { id: tileGrid columns: 4 Layout.fillWidth: true property int tileSize: 90 property int tileIconSize: 64 ImageButton { property string url: "http://docs.resp.app/en/latest/quick-start/#connect-to-a-local-or-public-redis-server" tooltip: url Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "Local or Public Redis") showBorder: true iconSource: "" onClicked: Qt.openUrlExternally(url) } ImageButton { property string url: "http://docs.resp.app/en/latest/quick-start/#connect-to-a-public-redis-server-with-ssl" tooltip: url Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "Redis with SSL/TLS") showBorder: true iconSource: "" onClicked: Qt.openUrlExternally(url) } ImageButton { property string url: "http://docs.resp.app/en/latest/quick-start/#connect-to-private-redis-server-via-ssh-tunnel" tooltip: url Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "SSH tunnel") showBorder: true iconSource: "" onClicked: Qt.openUrlExternally(url) } ImageButton { property string url: "http://docs.resp.app/en/latest/quick-start/#connect-to-a-unix-socket" tooltip: url Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "UNIX socket") showBorder: true iconSource: "" onClicked: Qt.openUrlExternally(url) } ImageButton { property string url: "http://docs.resp.app/en/latest/quick-start/#digital-ocean-managed-redis" Layout.fillWidth: true implicitHeight: tileGrid.tileSize imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: "qrc:/images/digitalocean_logo.svg" showBorder: true tooltip: url onClicked: Qt.openUrlExternally(url) } ImageButton { property string url: "http://docs.resp.app/en/latest/quick-start/#microsoft-azure-redis-cache" Layout.fillWidth: true implicitHeight: tileGrid.tileSize imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: "qrc:/images/azure_logo.svg" showBorder: true tooltip: url onClicked: Qt.openUrlExternally(url) } ImageButton { property string url: "http://docs.resp.app/en/latest/quick-start/#aws-elasticache" Layout.fillWidth: true implicitHeight: tileGrid.tileSize imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: approot.darkModeEnabled? "qrc:/images/aws_logo_white.svg" : "qrc:/images/aws_logo.svg" showBorder: true tooltip: url onClicked: Qt.openUrlExternally(url) } ImageButton { property string url: "http://docs.resp.app/en/latest/quick-start/#heroku-redis" Layout.fillWidth: true implicitHeight: tileGrid.tileSize imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: "qrc:/images/heroku_logo.svg" showBorder: true tooltip: url onClicked: Qt.openUrlExternally(url) } } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.topMargin: 10 SettingsGroupTitle { text: qsTranslate("RESP",'Cannot figure out how to connect to your redis-server?') } RichTextWithLinks { Layout.fillWidth: true wrapMode: Text.WrapAnywhere html: qsTranslate("RESP",'Read the Docs, ' + 'Contact Support ' + 'or ask for help in our Telegram Group') } } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.topMargin: 10 SettingsGroupTitle { text: qsTranslate("RESP","Don't have running Redis?") } RichTextWithLinks { Layout.fillWidth: true wrapMode: Text.WrapAnywhere html: '' + qsTranslate("RESP",'Use Redis Cloud: Up to 6 month free with $200 credits') + '' } } RowLayout { Item { Layout.fillWidth: true } Item { Layout.fillHeight: true } BetterButton { id: skipToNextTabBtn text: qsTranslate("RESP","Skip") onClicked: { connectionSettingsTabBar.currentIndex = 1 } Keys.onEnterPressed: { skipToNextTabBtn.clicked() } Keys.onReturnPressed: { skipToNextTabBtn.clicked() } } } } } BetterTab { id: mainTab Layout.fillWidth: true Layout.fillHeight: true Layout.margins: 30 clip: true ColumnLayout { anchors.fill: parent anchors.margins: 10 GridLayout { objectName: "rdm_connection_basic_settings" columns: 2 Layout.fillWidth: true BetterLabel { text: qsTranslate("RESP","Name:") } BetterTextField { id: connectionName objectName: "rdm_connection_name_field" Layout.fillWidth: true placeholderText: qsTranslate("RESP","Connection Name") text: root.settings ? root.settings.name : "" Component.onCompleted: root.items.push(connectionName) onTextChanged: root.settings.name = text } BetterLabel { text: qsTranslate("RESP","Address:") } AddressInput { id: connectionAddress objectName: "rdm_connection_address_input" placeholderText: qsTranslate("RESP","redis-server host") host: root.settings ? root.settings.host : "" port: root.settings ? root.settings.port : 0 Component.onCompleted: root.items.push(connectionAddress) onHostChanged: if (root.settings) root.settings.host = host onPortChanged: if (root.settings) root.settings.port = port } BetterLabel { id: windowsLocalhostWarning Layout.columnSpan: 2 text: qsTranslate("RESP", "For better network performance please use 127.0.0.1") visible: !root.sshEnabled && !root.sslEnabled && String(connectionAddress.host).toLowerCase() === "localhost" && Qt.platform.os == "windows" } BetterLabel { text: qsTranslate("RESP","Password:") } PasswordInput { id: connectionAuth objectName: "rdm_connection_auth_field" Layout.fillWidth: true placeholderText: qsTranslate("RESP","(Optional) redis-server authentication password") text: root.settings ? root.settings.auth : "" onTextChanged: root.settings.auth = text } BetterLabel { text: qsTranslate("RESP","Username:") } BetterTextField { id: connectionUsername objectName: "rdm_connection_username_field" Layout.fillWidth: true placeholderText: qsTranslate("RESP","(Optional) redis-server authentication username" + " (Redis >6.0)") text: root.settings ? root.settings.username : "" onTextChanged: if (root.settings) root.settings.username = text } } Item { Layout.preferredWidth: 10 } SettingsGroupTitle { text: qsTranslate("RESP","Security") } GridLayout { id: securityGrid objectName: "rdm_connection_group_box_security" columns: 2 RowLayout { Layout.fillWidth: true Layout.columnSpan: 2 BetterRadioButton { id: sslRadioButton objectName: "rdm_connection_security_ssl_radio_button" text: qsTranslate("RESP","SSL / TLS") allowUncheck: true checked: root.settings ? root.settings.sslEnabled && !root.sshEnabled : false Component.onCompleted: root.sslEnabled = Qt.binding(function() { return sslRadioButton.checked }) onCheckedChanged: { root.settings.sslEnabled = checked root.cleanStyle() } } BetterRadioButton { id: sshRadioButton objectName: "rdm_connection_security_ssh_radio_button" text: qsTranslate("RESP","SSH Tunnel") allowUncheck: true checked: root.settings ? root.settings.useSshTunnel() : false Component.onCompleted: root.sshEnabled = Qt.binding(function() { return sshRadioButton.checked }) onCheckedChanged: { root.cleanStyle() } } } Item { Layout.preferredWidth: 15 } GridLayout { id: tlsSettingsGrid objectName: "rdm_connection_security_ssl_grid" enabled: sslRadioButton.checked visible: sslRadioButton.checked columns: 2 Layout.fillWidth: true BetterLabel { text: qsTranslate("RESP","Public Key:") } FilePathInput { id: sslLocalCertPath objectName: "rdm_connection_security_ssl_local_cert_path_field" Layout.fillWidth: true placeholderText: qsTranslate("RESP","(Optional) Public Key in PEM format") nameFilters: [ "Public Key in PEM format (*.pem *.crt)" ] title: qsTranslate("RESP","Select public key in PEM format") path: root.settings ? root.settings.sslLocalCertPath : "" onPathChanged: root.settings.sslLocalCertPath = path } BetterLabel { text: qsTranslate("RESP", "Private Key") + ":" } FilePathInput { id: sslPrivateKeyPath objectName: "rdm_connection_security_ssl_private_key_path_field" Layout.fillWidth: true placeholderText: qsTranslate("RESP","(Optional) Private Key in PEM format") nameFilters: [ "Private Key in PEM format (*.pem *.key)" ] title: qsTranslate("RESP","Select private key in PEM format") path: root.settings ? root.settings.sslPrivateKeyPath : "" onPathChanged: root.settings.sslPrivateKeyPath = path } BetterLabel { text: qsTranslate("RESP","Authority:") } FilePathInput { id: sslCaCertPath objectName: "rdm_connection_security_ssl_ca_cert_path_field" Layout.fillWidth: true placeholderText: qsTranslate("RESP","(Optional) Authority in PEM format") nameFilters: [ "Authority file in PEM format (*.pem *.crt)" ] title: qsTranslate("RESP","Select authority file in PEM format") path: root.settings ? root.settings.sslCaCertPath : "" onPathChanged: root.settings.sslCaCertPath = path } BetterLabel { text: qsTranslate("RESP","Enable strict mode:")} BetterCheckbox { id: ignoreSSLErrors Layout.fillWidth: true checked: root.settings ? !root.settings.ignoreSSLErrors : false onCheckedChanged: root.settings.ignoreSSLErrors = !checked } } GridLayout { id: sshSettingsGrid objectName: "rdm_connection_security_ssh_grid" visible: sshRadioButton.checked enabled: sshRadioButton.checked columns: 2 Layout.fillWidth: true BetterLabel { text: qsTranslate("RESP","SSH Address:") } AddressInput { id: sshAddress placeholderText: qsTranslate("RESP","Remote Host with SSH server") port: root.settings ? root.settings.sshPort : 22 host: root.settings ? root.settings.sshHost : "" Component.onCompleted: root.sshItems.push(sshAddress) onHostChanged: root.settings.sshHost = host onPortChanged: root.settings.sshPort = port } BetterLabel { text: qsTranslate("RESP","SSH User:") } BetterTextField { id: sshUser objectName: "rdm_connection_security_ssh_user_field" Layout.fillWidth: true placeholderText: qsTranslate("RESP","Valid SSH User Name") text: root.settings ? root.settings.sshUser : "" Component.onCompleted: root.sshItems.push(sshUser) onTextChanged: root.settings.sshUser = text } BetterCheckbox { id: sshAgentCheckbox objectName: "rdm_connection_security_ssh_agent" text: qsTranslate("RESP","Use SSH Agent") checked: root.settings ? root.settings.sshAgent : false onCheckedChanged: root.settings.sshAgent = checked } FilePathInput { id: sshAgentPath visible: !(Qt.platform.os === "windows" || (PlatformUtils.isOSX() && qmlUtils.isAppStoreBuild())) objectName: "rdm_connection_security_ssh_agent_path_field" Layout.fillWidth: true placeholderText: qsTranslate("RESP","(Optional) Custom SSH Agent Path") nameFilters: [ "SSH Agent (*)" ] title: qsTranslate("RESP","Select SSH Agent") path: root.settings ? root.settings.sshAgentPath : "" onPathChanged: root.settings.sshAgentPath = path } RichTextWithLinks { visible: PlatformUtils.isOSX() && qmlUtils.isAppStoreBuild() Layout.fillWidth: true wrapMode: Text.WrapAnywhere html: '' + qsTranslate("RESP",'Additional configuration is required to enable SSH Agent support') + '' } BetterGroupbox { id: sshKeyGroupBox labelText: qsTranslate("RESP","Private Key") objectName: "rdm_connection_security_ssh_key_group_box" checked: root.settings ? root.settings.sshPrivateKey : false enabled: !sshAgentCheckbox.checked opacity: enabled ? 1 : 0.5 Layout.columnSpan: 2 Layout.fillWidth: true ColumnLayout { anchors.fill: parent FilePathInput { id: sshPrivateKey objectName: "rdm_connection_security_ssh_key_path_field" Layout.fillWidth: true placeholderText: qsTranslate("RESP","Path to Private Key in PEM format") nameFilters: [ "Private key in PEM format (*)" ] title: qsTranslate("RESP","Select private key in PEM format") path: root.settings ? root.settings.sshPrivateKey : "" onPathChanged: root.settings.sshPrivateKey = path } BetterLabel { visible: PlatformUtils.isOSX() Layout.fillWidth: true; text: qsTranslate("RESP","Tip: Use ⌘ + Shift + . to show hidden files and folders in dialog") } } } BetterGroupbox { id: sshPasswordGroupBox labelText: sshKeyGroupBox.checked? qsTranslate("RESP","Passphrase") : qsTranslate("RESP","Password") objectName: "rdm_connection_security_ssh_password_group_box" checked: root.settings ? root.settings.sshPassword || root.settings.askForSshPassword : true enabled: !sshAgentCheckbox.checked opacity: enabled ? 1 : 0.5 Layout.columnSpan: 2 Layout.fillWidth: true RowLayout { anchors.fill: parent PasswordInput { id: sshPassword objectName: "rdm_connection_security_ssh_password_field" Layout.fillWidth: true placeholderText: sshKeyGroupBox.checked? qsTranslate("RESP","Passphrase for provided private key") : sshAskForPasswordCheckbox.checked? qsTranslate("RESP","Password request will be prompt prior to connection") : qsTranslate("RESP","SSH User Password") text: root.settings ? root.settings.sshPassword : "" onTextChanged: root.settings.sshPassword = text enabled: !sshAskForPasswordCheckbox.checked } BetterCheckbox { id: sshAskForPasswordCheckbox objectName: "rdm_connection_security_ssh_ask_for_password" text: qsTranslate("RESP","Ask for password") checked: root.settings ? root.settings.askForSshPassword : false onCheckedChanged: root.settings.askForSshPassword = checked } } } BetterCheckbox { id: sshTLSoverSSHCheckbox objectName: "rdm_connection_security_ssh_tls_over_ssh" Layout.fillWidth: true Layout.columnSpan: 2 text: qsTranslate("RESP","Enable TLS-over-SSH (AWS ElastiCache Encryption in-transit)") checked: root.settings ? root.settings.sslEnabled : false onCheckedChanged: root.settings.sslEnabled = checked Connections { target: root function onSslEnabledChanged() { // NOTE(u_glide): Workaround for case when user enables plain TLS // on existing TLS-over-SSH connection and then selects SSH again. if (!root.sslEnabled && root.settings.sshHost && sshTLSoverSSHCheckbox.checked) { root.settings.sslEnabled = true } } } } } } Item { Layout.fillHeight: true } } } BetterTab { Layout.fillWidth: true Layout.fillHeight: true Layout.margins: 30 GridLayout { anchors.fill: parent anchors.margins: 10 columns: 2 SettingsGroupTitle { text: qsTranslate("RESP","Keys loading") Layout.columnSpan: 2 } BetterLabel { text: qsTranslate("RESP","Default filter:") } BetterTextField { id: keysPattern Layout.fillWidth: true placeholderText: qsTranslate("RESP","Pattern which defines loaded keys from redis-server") text: root.settings ? root.settings.keysPattern : "" Component.onCompleted: root.items.push(keysPattern) onTextChanged: if (root.settings) { root.settings.keysPattern = text } } BetterLabel { text: qsTranslate("RESP","Namespace Separator:") } BetterTextField { id: namespaceSeparator Layout.fillWidth: true objectName: "rdm_advanced_settings_namespace_separator_field" placeholderText: qsTranslate("RESP","Separator used for namespace extraction from keys") text: root.settings ? root.settings.namespaceSeparator : "" onTextChanged: if (root.settings) { root.settings.namespaceSeparator = text } } SettingsGroupTitle { text: qsTranslate("RESP","Timeouts & Limits") Layout.columnSpan: 2 } BetterLabel { text: qsTranslate("RESP","Connection Timeout (sec):") } BetterSpinBox { id: executeTimeout Layout.fillWidth: true from: 10 to: 100000 value: { return root.settings ? (root.settings.executeTimeout / 1000.0) : 0 } onValueChanged: if (root.settings) { root.settings.executeTimeout = value * 1000 } } BetterLabel { text: qsTranslate("RESP","Execution Timeout (sec):")} BetterSpinBox { id: connectionTimeout Layout.fillWidth: true from: 10 to: 100000 value: root.settings ? (root.settings.connectionTimeout / 1000.0) : 0 onValueChanged: if (root.settings) { root.settings.connectionTimeout = value * 1000 } } BetterLabel { text: qsTranslate("RESP","Databases discovery limit:") } BetterSpinBox { id: dbScanLimit Layout.fillWidth: true from: 1 to: 100000 value: { return root.settings ? root.settings.databaseScanLimit : 1 } onValueChanged: if (root.settings) { root.settings.databaseScanLimit = value } } SettingsGroupTitle { text: qsTranslate("RESP","Cluster") Layout.columnSpan: 2 } BetterLabel { text: qsTranslate("RESP","Change host on cluster redirects:")} BetterCheckbox { id: overrideClusterHost Layout.fillWidth: true checked: root.settings ? root.settings.overrideClusterHost : false onCheckedChanged: if (root.settings) { root.settings.overrideClusterHost = checked } } SettingsGroupTitle { text: qsTranslate("RESP","Formatters") Layout.columnSpan: 2 } BetterLabel { text: qsTranslate("RESP","Default value formatter:")} RowLayout { Layout.fillWidth: true BetterComboBox { id: defaultFormatterLogicSelector Layout.fillWidth: true property int customFormatterIndex: 2 ListModel { id: defaultFormatterOptionsModel Component.onCompleted: { append({ value: "auto", text: qsTranslate("RESP", "Auto detect (JSON / Plain Text / HEX)") }) append({ value: "last_selected", text: qsTranslate("RESP", "Last selected") }) append({ value: "specific", text: qsTranslate("RESP", "Select formatter ...") }) defaultFormatterLogicSelector.currentIndex = 0 } } textRole: "text" model: defaultFormatterOptionsModel Connections { target: root function onSettingsChanged(s) { if (!root.settings) { defaultFormatterLogicSelector.currentIndex = 0; return; } if (root.settings.defaultFormatter !== "auto" && root.settings.defaultFormatter !== "last_selected") { defaultFormatterSelector._select(root.settings.defaultFormatter) defaultFormatterLogicSelector.currentIndex = defaultFormatterLogicSelector.customFormatterIndex; return; } defaultFormatterLogicSelector.currentIndex = root.settings.defaultFormatter === "auto"? 0 : 1; } } onActivated: { if (currentIndex != customFormatterIndex) { root.settings.defaultFormatter = defaultFormatterOptionsModel.get(currentIndex)["value"] } } } BetterComboBox { id: defaultFormatterSelector visible: defaultFormatterLogicSelector.currentIndex == 2 model: valueFormattersModel textRole: "name" onActivated: { root.settings.defaultFormatter = currentText } } } SettingsGroupTitle { text: qsTranslate("RESP","Appearance") Layout.columnSpan: 2 } BetterLabel { text: qsTranslate("RESP","Icon color:")} ColorInput { id: iconsColor Layout.fillWidth: true color: root.settings ? root.settings.iconColor : "" onColorChanged: root.settings.iconColor = color Connections { target: root function onResetSettings() { iconsColor.reset(); } } } Item { Layout.columnSpan: 2 Layout.fillHeight: true Layout.fillWidth: true } } } } RowLayout { Layout.fillWidth: true Layout.preferredHeight: 30 visible: !isNewConnection || isNewConnection && connectionSettingsTabBar.currentIndex != 0 BetterButton { objectName: "rdm_connection_settings_dialog_test_btn" iconSource: PlatformUtils.getThemeIcon("offline.svg") text: qsTranslate("RESP","Test Connection") onClicked: { showLoader() root.testConnection(root.settings) } } BetterButton { iconSource: PlatformUtils.getThemeIcon("help.svg") text: qsTranslate("RESP","Quick Start Guide") onClicked: Qt.openUrlExternally(root.quickStartGuideUrl) visible: !isNewConnection } Item { Layout.fillWidth: true } RowLayout { id: validationWarning visible: false Layout.fillWidth: true Image { width: 15 height: 15 sourceSize.width: 30 sourceSize.height: 30 source: PlatformUtils.getThemeIcon("alert.svg") } BetterLabel { text: qsTranslate("RESP","Invalid settings detected!") } } Item { Layout.fillWidth: true } BetterButton { objectName: "rdm_connection_settings_dialog_ok_btn" text: qsTranslate("RESP","OK") onClicked: { if (root.validate()) { if (!sshKeyGroupBox.checked) root.settings.sshPrivateKey = "" if (!sshPasswordGroupBox.checked) root.settings.sshPassword = "" if (sshAgentCheckbox.checked) { root.settings.sshPrivateKey = "" root.settings.sshPassword = "" } else { root.settings.sshAgentPath = "" } root.saveConnection(root.settings) root.settings = connectionsManager.createEmptyConfig() root.resetSettings() root.close() } else { validationWarning.visible = true } } } BetterButton { objectName: "rdm_connection_settings_dialog_cancel_btn" text: qsTranslate("RESP","Cancel") onClicked: { root.settings = connectionsManager.createEmptyConfig() root.cleanStyle() root.resetSettings() root.close() } } } } Rectangle { id: uiBlocker visible: false anchors.fill: parent color: Qt.rgba(0, 0, 0, 0.1) Item { anchors.fill: parent BusyIndicator { anchors.centerIn: parent; running: true } } MouseArea { anchors.fill: parent } } OkDialogOverlay { id: dialog_notification objectName: "rdm_qml_connection_settings_error_dialog" visible: false function showError(msg) { text = msg title = qsTranslate("RESP","Error") open() } function showMsg(msg) { text = msg title = qsTranslate("RESP","Success") open() } } } } } ================================================ FILE: src/qml/connections-tree/BetterTreeView.qml ================================================ import QtQuick 2.14 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQml.Models 2.2 import QtQuick.Window 2.2 import "./../common/platformutils.js" as PlatformUtils import "./../common" import "." TreeView { id: root alternatingRowColors: false headerVisible: false focus: true horizontalScrollBarPolicy: Qt.ScrollBarAsNeeded verticalScrollBarPolicy: Qt.ScrollBarAsNeeded model: connectionsManager property bool sortConnections: false backgroundVisible: false /** * NOTE(u_glide): Dirty hack to use build-in macOS style for scrollbars on all platforms */ Component.onCompleted: { if (!PlatformUtils.isOSX()) { __scroller.verticalScrollBar.__panel.on = true } } Connections { target: !PlatformUtils.isOSX()? __scroller.verticalScrollBar.__panel : null function onOnChanged() { if (!__scroller.verticalScrollBar.__panel.on) { __scroller.verticalScrollBar.__panel.on = true } } } Component { id: patchedBackground Item { implicitWidth: 25 implicitHeight: 200 } } // hack-end style: TreeViewStyle { frame: Item {} indentation: 12 rowDelegate: Rectangle { height: PlatformUtils.isOSXRetina(Screen) ? 25 : 30 color: styleData.selected ? sysPalette.highlight : "transparent" } transientScrollBars: true backgroundColor: sysPalette.button scrollBarBackground: PlatformUtils.isOSX()? TreeViewStyle.scrollBarBackground : patchedBackground } TableViewColumn { id: itemColumn title: "item" role: "metadata" delegate: TreeItemDelegate { id: itemRoot treeRoot: root sortConnections: root.sortConnections } } selectionMode: SelectionMode.SingleSelection selection: ItemSelectionModel { id: connectionTreeSelectionModel model: connectionsManager } onClicked: connectionsManager && connectionsManager.sendEvent(index, "click") onExpanded: connectionsManager.setExpanded(index) onCollapsed: connectionsManager.setCollapsed(index) Connections { target: connectionsManager function onExpand(index) { if (root.isExpanded(index)) return root.expand(index) } } } ================================================ FILE: src/qml/connections-tree/ConnectionGroupDialog.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.13 import QtQuick.Window 2.3 import "../common" import "../common/platformutils.js" as PlatformUtils BetterDialog { id: root title: group? qsTranslate("RESP","Edit Connections Group") + group.name : qsTranslate("RESP","Add New Connections Group") visible: false property var group footer: null signal addNewGroup(string name) signal editGroup(var group) Item { anchors.fill: parent implicitHeight: 150 implicitWidth: 600 ColumnLayout { anchors.fill: parent anchors.margins: 5 BetterLabel { text: qsTranslate("RESP","Group Name:") } BetterTextField { id: groupName Layout.fillWidth: true objectName: "rdm_connections_group_field" text: group? group.name : '' } RowLayout { Layout.fillWidth: true Layout.minimumHeight: 40 Item { Layout.fillWidth: true } BetterButton { objectName: "rdm_connections_group_save_btn" text: qsTranslate("RESP","Save") onClicked: { if (group) { group.name = groupName.text root.editGroup(group) } else { root.addNewGroup(groupName.text) } root.close() } } BetterButton { text: qsTranslate("RESP","Cancel") onClicked: root.close() } } Item { Layout.fillWidth: true } } } } ================================================ FILE: src/qml/connections-tree/TreeItemDelegate.qml ================================================ import QtQuick 2.14 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQml.Models 2.2 import QtQuick.Window 2.2 import "./../common/platformutils.js" as PlatformUtils import "./../common" FocusScope { id: root property bool sortConnections: false property var treeRoot function __getIconColorMappings(userColor) { if (!userColor) return {} if (approot.darkModeEnabled) { return { "unknown": { "#979798": qmlUtils.changeColorAlpha(userColor, 150) }, "standalone": { "#DC423C": userColor, "#9A2928": qmlUtils.changeColorAlpha(userColor, 200) }, "cluster": { "#5856D6": userColor }, "sentinel": { "#DC423C": userColor }, "database": { "#DC423C": userColor } } } else { return { "unknown": { "#B8BEC9": qmlUtils.changeColorAlpha(userColor, 150) }, "standalone": { "#DC423C": userColor, "#9A2928": qmlUtils.changeColorAlpha(userColor, 200) }, "cluster": { "#5E5CE6": userColor }, "sentinel": { "#DC423C": userColor }, "database": { "#DC423C": userColor } } } } MouseArea { id: dragArea anchors.fill: parent acceptedButtons: root.sortConnections ? Qt.LeftButton : Qt.RightButton | Qt.MiddleButton drag.target: root.sortConnections ? wrapper : null drag.axis: Drag.YAxis property bool held: false hoverEnabled: true propagateComposedEvents: !root.sortConnections onReleased: { if (root.sortConnections) { wrapper.Drag.drop() held = false wrapper.color = "transparent" wrapper.border.width = 0 return } } onPressed: { if (root.sortConnections && styleData.value["type"] === "server") { held = true wrapper.border.width = 1 wrapper.border.color = sysPalette.light return } } onClicked: { console.log("Catch event to item") if (mouse.button === Qt.RightButton) { mouse.accepted = true connectionTreeSelectionModel.setCurrentIndex(styleData.index, 1) connectionsManager.sendEvent(styleData.index, "right-click") return } if (mouse.button === Qt.MiddleButton) { mouse.accepted = true connectionsManager.sendEvent(styleData.index, "mid-click") return } } Rectangle { id: wrapper objectName: "rdm_tree_view_item" height: PlatformUtils.isOSXRetina(Screen) ? 20 : 30 anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.rightMargin: 10 color: "transparent" property var itemIndex: styleData.index Drag.active: dragArea.held Drag.hotSpot.x: width / 3 Drag.hotSpot.y: height / 3 states: State { when: dragArea.held ParentChange { target: wrapper parent: treeRoot } PropertyChanges { target: wrapper anchors.leftMargin: 30 } AnchorChanges { target: wrapper anchors { verticalCenter: undefined } } } property bool itemEnabled: styleData.value["state"] === true property bool itemLocked: styleData.value["locked"] === true property string itemType: styleData.value["type"] ? styleData.value["type"] : "" property string userColor: styleData.value["user_color"] ? styleData.value["user_color"] : "" property string itemIconSource: { if (itemLocked) { return PlatformUtils.getThemeIcon("wait.svg") } var type = itemType if (type === "server") { var server_type = styleData.value["server_type"] var serverIcon = "" if (server_type === "unknown") { serverIcon = PlatformUtils.getThemeIcon( "server_offline.svg") } else if (server_type === "standalone") { serverIcon = PlatformUtils.getThemeIcon("server.svg") } else { serverIcon = PlatformUtils.getThemeIcon( server_type + ".svg") } if (userColor) { return qmlUtils.replaceColorsInSvg( serverIcon, root.__getIconColorMappings( userColor)[server_type]) } else { return serverIcon } } else if (type === "database") { if (styleData.value["live_update"] === true) { return PlatformUtils.getThemeIcon("live_update.svg") } else { var icon = PlatformUtils.getThemeIcon(type + ".svg") if (userColor) { return qmlUtils.replaceColorsInSvg( icon, root.__getIconColorMappings( userColor)[type]) } else { return icon } } } else if (type === "namespace" || type == "server_group" && styleData.isExpanded) { return PlatformUtils.getThemeIcon(type + "_open.svg") } else { if (type !== "") { return PlatformUtils.getThemeIcon(type + ".svg") } else { return "" } } } Image { id: itemIcon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter sourceSize.width: 25 sourceSize.height: 25 source: wrapper ? wrapper.itemIconSource : "" cache: true asynchronous: true } Text { objectName: "rdm_tree_view_item_text" anchors.left: itemIcon.right anchors.leftMargin: 3 anchors.verticalCenter: parent.verticalCenter text: wrapper.itemEnabled ? styleData.value["name"] : styleData.value["name"] + qsTranslate("RESP", " (Removed)") color: wrapper.itemEnabled ? styleData.selected ? sysPalette.highlightedText : sysPalette.windowText : inactiveSysPalette.windowText } Rectangle { id: menuWrapper implicitWidth: styleData.value["type"] === "database"? 200 : 150 anchors { right: wrapper.right top: wrapper.top bottom: wrapper.bottom rightMargin: 20 } height: parent.height visible: styleData.selected && wrapper.itemEnabled color: { var baseColor = sysPalette.highlight; if (styleData.selected) { return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.85) } else { return "transparent" } } Loader { id: menuLoader anchors { right: menuWrapper.right top: menuWrapper.top bottom: menuWrapper.bottom } height: parent.height asynchronous: false source: { if (!(styleData.selected && styleData.value["type"])) return "" return "./menu/" + styleData.value["type"] + ".qml" } onLoaded: { wrapper.forceActiveFocus() menuWrapper.width = item.width } } } focus: true Keys.forwardTo: menuLoader.item ? [menuLoader.item] : [] } DropArea { anchors { fill: parent } onEntered: { if (styleData.value["type"] === "server_group") { wrapper.border.width = 1 wrapper.border.color = sysPalette.highlight } } onDropped: { wrapper.border.width = 0 connectionsManager.dropItemAt(drag.source.itemIndex, styleData.index) } onExited: { wrapper.border.width = 0 } } } } ================================================ FILE: src/qml/connections-tree/menu/InlineMenu.qml ================================================ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import QtQuick.Window 2.2 import "./../../common/platformutils.js" as PlatformUtils import "./../../" import "./../../common/" RowLayout { id: root property alias model: repeater.model property var callbacks function sendEvent(e) { if (!connectionsManager) return connectionsManager.sendEvent(styleData.index, e) } function callCallback(c) { return callbacks[c]() } Repeater { id: repeater Item { Layout.preferredWidth: PlatformUtils.isOSXRetina(Screen)? 20 : 25 Layout.preferredHeight: PlatformUtils.isOSXRetina(Screen)? 20 : 25 Layout.maximumHeight: PlatformUtils.isOSXRetina(Screen)? 20 : 25 ImageButton { id: actionButton anchors.fill: parent iconSource: modelData['icon'] imgWidth: PlatformUtils.isOSXRetina(Screen)? 20 : 25 imgHeight: PlatformUtils.isOSXRetina(Screen)? 20 : 25 onClicked: handleClick() function handleClick() { if (modelData['callback'] != undefined) return root.callCallback(modelData['callback']) else return root.sendEvent(modelData['event']) } tooltip: modelData['help'] != undefined ? modelData['help'] + (modelData["shortcut"]? " (" + shortcut.nativeText + ")" : "") : "" objectName: { if (modelData['event'] != undefined) return "rdm_inline_menu_button_" + modelData['event'] if (modelData['callback'] != undefined) return "rdm_inline_menu_button_" + modelData['callback'] return "" } } Shortcut { id: shortcut sequence: modelData["shortcut"] onActivated: actionButton.handleClick() } } } } ================================================ FILE: src/qml/connections-tree/menu/database.qml ================================================ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.13 import QtQuick.Window 2.2 import "./../../common/platformutils.js" as PlatformUtils import "." import "./../../common/" RowLayout { id: root focus: true spacing: 0 state: "menu" states: [ State { name: "menu" PropertyChanges { target: dbMenu; visible: true;} PropertyChanges { target: bulkMenu; visible: false;} PropertyChanges { target: filterMenu; visible: false;} }, State { name: "bulk_menu" PropertyChanges { target: dbMenu; visible: false;} PropertyChanges { target: bulkMenu; visible: true;} PropertyChanges { target: filterMenu; visible: false;} }, State { name: "filter" PropertyChanges { target: dbMenu; visible: false;} PropertyChanges { target: bulkMenu; visible: false;} PropertyChanges { target: filterMenu; visible: true;} } ] Keys.onPressed: { if (state == "filter" && event.key == Qt.Key_Escape) { state = "menu" } } InlineMenu { id: dbMenu Layout.fillWidth: true callbacks: { "filter": function() { root.state = "filter" filterCombobox.currentIndex = filterCombobox.find(styleData.value["filter"]) filterCombobox.editText = styleData.value["filter"] }, "live_update": function () { if (styleData.value["live_update"]) { connectionsManager.setMetadata(styleData.index, "live_update", '') } else { connectionsManager.setMetadata(styleData.index, "live_update", true) } }, "bulk_menu": function() { root.state = "bulk_menu" }, } model: { if (styleData.value["locked"] === true) { return [ { 'icon': PlatformUtils.getThemeIcon("offline.svg"), 'event': 'cancel', "help": qsTranslate("RESP","Disconnect"), }, ] } else { return [ { 'icon': PlatformUtils.getThemeIcon("filter.svg"), 'callback': 'filter', "help": qsTranslate("RESP","Open Keys Filter"), "shortcut": "Ctrl+F", }, { 'icon': PlatformUtils.getThemeIcon("refresh.svg"), 'event': 'reload', "help": qsTranslate("RESP","Reload Keys in Database"), "shortcut": "Ctrl+R", }, { 'icon': PlatformUtils.getThemeIcon("add.svg"), 'event': 'add_key', "help": qsTranslate("RESP","Add New Key"), "shortcut": "Ctrl+N", }, { 'icon': styleData.value["live_update"]? PlatformUtils.getThemeIcon("live_update_disable.svg") : PlatformUtils.getThemeIcon("live_update.svg"), 'callback': 'live_update', "help": styleData.value["live_update"]? qsTranslate("RESP","Disable Live Update") : qsTranslate("RESP","Enable Live Update"), "shortcut": "Ctrl+L", }, { 'icon': PlatformUtils.getThemeIcon("console.svg"), 'event': 'console', "help": qsTranslate("RESP","Open Console"), "shortcut": "Ctrl+T", }, {'icon': PlatformUtils.getThemeIcon("memory_usage.svg"), "event": "analyze_memory_usage", "help": qsTranslate("RESP","Analyze Used Memory")}, { 'icon': PlatformUtils.getThemeIcon("bulk_operations.svg"), 'callback': 'bulk_menu', "help": qsTranslate("RESP","Bulk Operations"), }, ] } } } InlineMenu { id: bulkMenu Layout.fillWidth: true callbacks: { "db_menu": function() { root.state = "menu" }, } model: { return [ { 'icon': PlatformUtils.getThemeIcon("cleanup.svg"), 'event': 'flush', "help": qsTranslate("RESP","Flush Database"), }, { 'icon': PlatformUtils.getThemeIcon("cleanup_filtered.svg"), 'event': 'delete_keys', "help": qsTranslate("RESP","Delete keys with filter"), }, { 'icon': PlatformUtils.getThemeIcon("ttl.svg"), 'event': 'ttl', "help": qsTranslate("RESP","Set TTL for multiple keys"), }, { 'icon': PlatformUtils.getThemeIcon("db-copy.svg"), 'event': 'copy_keys', "help": qsTranslate("RESP","Copy keys from this database to another"), }, { 'icon': PlatformUtils.getThemeIcon("import.svg"), 'event': 'rdb_import', "help": qsTranslate("RESP","Import keys from RDB file"), }, { 'icon': PlatformUtils.getThemeIcon("back.svg"), 'callback': 'db_menu', "help": qsTranslate("RESP","Back"), }, ] } } RowLayout { id: filterMenu Layout.fillWidth: true Layout.fillHeight: true Layout.topMargin: PlatformUtils.isOSX() ? -3 : 0 property int btnWidth: PlatformUtils.isOSXRetina(Screen)? 18 : 22 property int btnHeight: PlatformUtils.isOSXRetina(Screen)? 18 : 22 BetterComboBox { id: filterCombobox objectName: "rdm_inline_menu_filter_field" editable: true Layout.preferredWidth: connectionsTree.width * 0.4 Layout.preferredHeight: PlatformUtils.isOSX()? 25 : 30 indicator.width: PlatformUtils.isOSX()? 30 : 40 indicator.height: PlatformUtils.isOSX()? 25 : 30 selectTextByMouse: true editText: styleData.value["filter"] model: styleData.value["filterHistory"] palette.highlightedText: sysPalette.highlightedText delegate: ItemDelegate { height: filterCombobox.height width: filterCombobox.width highlighted: filterCombobox.highlightedIndex === index contentItem: Text { text: modelData color: parent.highlighted ? sysPalette.buttonText : sysPalette.text verticalAlignment: Text.AlignVCenter elide: Text.ElideRight BetterToolTip { title: modelData visible: parent.truncated && title && hovered } } } onAccepted: { filterOk.setFilter() focus = false } } ImageButton { id: filterOk implicitWidth: filterMenu.btnWidth implicitHeight: filterMenu.btnHeight imgWidth: filterMenu.btnWidth imgHeight: filterMenu.btnHeight iconSource: PlatformUtils.getThemeIcon("ok.svg") objectName: "rdm_inline_menu_button_apply_filter" onClicked: setFilter() function setFilter() { if (!connectionsManager) return connectionsManager.setMetadata(styleData.index, "filter", filterCombobox.editText) root.state = "menu" } } ImageButton { id: filterHelp implicitWidth: filterMenu.btnWidth implicitHeight: filterMenu.btnHeight imgWidth: filterMenu.btnWidth imgHeight: filterMenu.btnHeight iconSource: PlatformUtils.getThemeIcon("help.svg") onClicked: Qt.openUrlExternally("https://docs.resp.app/en/latest/lg-keyspaces/#use-specific-scan-filter-to-reduce-loaded-amount-of-keys") } ImageButton { id: filterCancel implicitWidth: filterMenu.btnWidth implicitHeight: filterMenu.btnHeight imgWidth: filterMenu.btnWidth imgHeight: filterMenu.btnHeight iconSource: PlatformUtils.getThemeIcon("clear.svg") objectName: "rdm_inline_menu_button_reset_filter" onClicked: { if (!connectionsManager) return connectionsManager.setMetadata(styleData.index, "filter", "") root.state = "menu" } } } } ================================================ FILE: src/qml/connections-tree/menu/key.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import "." import "./../../common/platformutils.js" as PlatformUtils InlineMenu { id: root callbacks: { "copy": function() { var result = styleData.value["full_name"] if (result) { qmlUtils.copyToClipboard(result) } }, } model: [ {'icon': PlatformUtils.getThemeIcon("copy.svg"), "callback": "copy", "help": qsTranslate("RESP","Copy Key Name"), "shortcut": "Ctrl+C"}, {'icon': PlatformUtils.getThemeIcon("delete.svg"), "event": "delete", "help": qsTranslate("RESP","Delete key"), "shortcut": "D"} ] } ================================================ FILE: src/qml/connections-tree/menu/namespace.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import "." import "./../../common/platformutils.js" as PlatformUtils InlineMenu { id: root callbacks: { "copy": function() { var result = styleData.value["full_path"] if (result) { qmlUtils.copyToClipboard(result + ":*") } }, } model: { if (styleData.value["locked"] === true) { return [ { 'icon': PlatformUtils.getThemeIcon("offline.svg"), 'event': 'cancel', "help": qsTranslate("RESP","Disconnect"), }, ] } else { [ {'icon': PlatformUtils.getThemeIcon("refresh.svg"), "event": "reload", "help": qsTranslate("RESP","Reload Namespace"), "shortcut": "Ctrl+R"}, {'icon': PlatformUtils.getThemeIcon("add.svg"), 'event': 'add_key', "help": qsTranslate("RESP","Add New Key")}, {'icon': PlatformUtils.getThemeIcon("copy.svg"), "callback": "copy", "help": qsTranslate("RESP","Copy Namespace Pattern"), "shortcut": "Ctrl+C"}, {'icon': PlatformUtils.getThemeIcon("memory_usage.svg"), "event": "analyze_memory_usage", "help": qsTranslate("RESP","Analyze Used Memory")}, {'icon': PlatformUtils.getThemeIcon("delete.svg"), "event": "delete", "help": qsTranslate("RESP","Delete Namespace"), "shortcut": "D"}, ] } } } ================================================ FILE: src/qml/connections-tree/menu/server.qml ================================================ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import "." import "./../../common/platformutils.js" as PlatformUtils InlineMenu { id: root model: { if (styleData.value["locked"] === true) { return [ { 'icon': PlatformUtils.getThemeIcon("offline.svg"), 'event': 'cancel', "help": qsTranslate("RESP","Disconnect"), }, ] } else { return [ { 'icon': PlatformUtils.getThemeIcon("refresh.svg"), 'event': 'reload', "help": qsTranslate("RESP","Reload Server"), "shortcut": "Ctrl+R", }, { 'icon': PlatformUtils.getThemeIcon("offline.svg"), 'event': 'unload', "help": qsTranslate("RESP","Unload All Data"), "shortcut": "Ctrl+U", }, { 'icon': PlatformUtils.getThemeIcon("settings.svg"), 'event': 'edit', "help": qsTranslate("RESP","Edit Connection Settings"), "shortcut": "Ctrl+E", }, { 'icon': PlatformUtils.getThemeIcon("copy.svg"), 'event': 'duplicate', "help": qsTranslate("RESP","Duplicate Connection"), "shortcut": "Ctrl+C", }, { 'icon': PlatformUtils.getThemeIcon("delete.svg"), 'event': 'delete', "help": qsTranslate("RESP","Delete Connection"), "shortcut": "D", }, ] } } } ================================================ FILE: src/qml/connections-tree/menu/server_group.qml ================================================ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import "." import "./../../common/platformutils.js" as PlatformUtils InlineMenu { id: root model: { return [ { 'icon': PlatformUtils.getThemeIcon("settings.svg"), 'event': 'edit', "help": qsTranslate("RESP","Edit Connection Group"), "shortcut": "Ctrl+E", }, { 'icon': PlatformUtils.getThemeIcon("delete.svg"), 'event': 'delete', "help": qsTranslate("RESP","Delete Connection Group"), "shortcut": "D", }, ] } } ================================================ FILE: src/qml/console/BaseConsole.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 1.4 TextArea { id: root function clear() { text = "" } wrapMode: TextEdit.WrapAnywhere textFormat: TextEdit.PlainText } ================================================ FILE: src/qml/console/Consoles.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.1 import QtQuick.Window 2.2 import "./../common" import "./../common/platformutils.js" as PlatformUtils Repeater { id: root BetterTab { id: tab objectName: "rdm_console_tab" Component { id: consoleTabButton BetterTabButton { icon.source: PlatformUtils.getThemeIcon("console.svg") text: tabName tooltip: tabName onCloseClicked: { consoleModel.closeTab(tabIndex) } } } Component.onCompleted: { var tabButton = consoleTabButton.createObject(tab); tabButton.self = tabButton; tabButton.tabRef = tab; tabBar.addItem(tabButton) tabBar.activateTabButton(tabButton) tabs.activateTab(tab) tabModel.init() } RedisConsole { id: redisConsole anchors.fill: parent Connections { target: tabModel function onChangePrompt(text, showPrompt) { redisConsole.setPrompt(text, showPrompt) } function onAddOutput(text, resultType) { redisConsole.addOutput(text, resultType) } } onExecCommand: tabModel.executeCommand(command) } } } ================================================ FILE: src/qml/console/RedisConsole.qml ================================================ import QtQuick 2.3 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.1 import "../common" import "../common/platformutils.js" as PlatformUtils import "." import rdm.models 1.0 Rectangle { id: root color: "#3A3A3A" property bool cursorInEditArea: false property string prompt property int promptPosition property int promptLength: prompt.length property alias busy: textArea.readOnly property string initText: "RESP.app Redis Console
" + qsTranslate("RESP","Connecting...") function setPrompt(txt, display) { console.log("set prompt: ", txt, display) prompt = txt if (display) displayPrompt(); } function displayPrompt() { textArea.insert(textArea.length, prompt) promptPosition = textArea.length - promptLength //textArea.cursorPosition = textArea.length - 1 } function clear() { textArea.clear() } function addOutput(text, type) { if (type == "error") { textArea.append("" + qmlUtils.escapeHtmlEntities(text) + '') } else { textArea.append("" + qmlUtils.escapeHtmlEntities(text) + '') } if (type == "complete" || type == "error") { textArea.blockAllInput = false textArea.append("
") displayPrompt() } } signal execCommand(string command) BaseConsole { id: textArea anchors.fill: parent backgroundVisible: false textColor: "yellow" readOnly: root.promptLength == 0 || blockAllInput textFormat: TextEdit.RichText menu: null property bool blockAllInput: false property int commandStartPos: root.promptPosition + root.promptLength function getCurrentCommand() { return getText(commandStartPos, length) } Keys.onPressed: { if (readOnly) { console.log("Console is read-only. Ignore Key presses.") return } var cursorInReadOnlyArea = cursorPosition < commandStartPos if (event.key == Qt.Key_Backspace && cursorPosition <= commandStartPos) { event.accepted = true console.log("Block backspace") return } if (event.key == Qt.Key_Left && cursorPosition <= commandStartPos) { event.accepted = true console.log("Block left arrow") return } if (((event.modifiers == Qt.NoModifier) || (event.modifiers & Qt.ShiftModifier)) && cursorInReadOnlyArea) { cursorPosition = length event.accepted = true console.log("Block Input in Read-Only area") return } if (event.matches(StandardKey.Undo) && cursorPosition == commandStartPos) { event.accepted = true console.log("Block Undo") return } if (selectionStart < commandStartPos && (event.matches(StandardKey.Cut) || event.key == Qt.Key_Delete || event.key == Qt.Key_Backspace)) { event.accepted = true console.log("Block Cut/Delete") return } if (event.matches(StandardKey.Paste)) { event.accepted = true console.log("Block Reach Text Input") hiddenBuffer.text = "" hiddenBuffer.paste() if (cursorInReadOnlyArea) cursorPosition = length insert(cursorPosition, hiddenBuffer.text.trim()) return } if (event.key == Qt.Key_Up || event.key == Qt.Key_Down) { var command; if (commandsHistoryModel.historyNavigation) { if (event.key == Qt.Key_Down) { command = commandsHistoryModel.getNextCommand() } else { command = commandsHistoryModel.getPrevCommand() } } else { command = commandsHistoryModel.getCurrentCommand() } remove(commandStartPos, length) insert(commandStartPos, command) event.accepted = true return } if (event.key == Qt.Key_Return && cursorPosition > commandStartPos) { var command = getText(commandStartPos, length) blockAllInput = true event.accepted = true if (command.toLowerCase() === "clear") { root.clear() root.displayPrompt() blockAllInput = false } else { root.execCommand(command) } commandsHistoryModel.appendCommand(command) } autocompleteModel.filterString = "^" + getText(commandStartPos, cursorPosition) + event.text } Component.onCompleted: { textArea.text = root.initText } MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: { menu.popup() } } Menu { id: menu MenuItem { text: qsTranslate("RESP","Clear") iconSource: PlatformUtils.getThemeIcon("cleanup.svg") onTriggered: { root.clear() root.displayPrompt() } } } } ColumnLayout { objectName: "rdm_autocomplete_results" height: 150 width: root.width - x - 50 x: textArea.cursorRectangle? textArea.cursorRectangle.x : 0 y: textArea.cursorRectangle? textArea.cursorRectangle.y + 20 : 0 z: 255 visible: { return cmdAutocomplete.rowCount > 0 && autocompleteModel.filterString.length > 0 && textArea.cursorPosition >= textArea.commandStartPos } TableView { id: cmdAutocomplete Layout.fillWidth: true Layout.fillHeight: true model: autocompleteModel headerVisible: true TableViewColumn { title: "Command" role: "name" width: 120 } TableViewColumn { title: qsTranslate("RESP","Arguments") role: "arguments" width: 250 } TableViewColumn { title: qsTranslate("RESP","Description") role: "summary" width: 350 } TableViewColumn { title: qsTranslate("RESP","Available since") role: "since" width: 60 } itemDelegate: Item { Text { anchors.fill: parent color: styleData.textColor elide: styleData.elideMode text: styleData.value wrapMode: Text.WrapAnywhere maximumLineCount: 1 } MouseArea { enabled: styleData.column === 2 || styleData.column === 0 anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { var commandName = "#" try { commandName = consoleAutocompleteModel.getRow( autocompleteModel.getOriginalRowIndex(styleData.row) )["name"] } catch(err) { console.log("Cannot get command name:", err) } if (styleData.column === 2) { Qt.openUrlExternally("https://redis.io/commands/" + commandName) } else { textArea.remove(textArea.commandStartPos, textArea.cursorPosition) textArea.insert(textArea.commandStartPos, commandName) autocompleteModel.filterString = commandName } } } } } RowLayout { Layout.minimumWidth: 150 Layout.minimumHeight: closeBtn.implicitHeight Item { Layout.fillWidth: true } Button { id: closeBtn text: qsTranslate("RESP","Close") onClicked: { autocompleteModel.filterString = "" } } } } SortFilterProxyModel { id: autocompleteModel source: consoleAutocompleteModel filterSyntax: SortFilterProxyModel.RegExp filterCaseSensitivity: Qt.CaseInsensitive filterRole: "name" } TextArea { id: hiddenBuffer visible: false textFormat: TextEdit.PlainText } ListModel { id: commandsHistoryModel property int currentIndex: 0 property bool historyNavigation: false function getCurrentCommand() { checkCurrentPos() var res = get(currentIndex) historyNavigation = true return res["cmd"] } function getNextCommand() { currentIndex += 1 return getCurrentCommand() } function getPrevCommand() { currentIndex -= 1 return getCurrentCommand() } function checkCurrentPos() { if (currentIndex >= count) currentIndex = commandsHistoryModel.count - 1 if (currentIndex < 0) currentIndex = 0 } function appendCommand(cmdStr) { append({"cmd": cmdStr}) currentIndex = count - 1 historyNavigation = false } } } ================================================ FILE: src/qml/dummy.qml ================================================ // Unused dummy QML file to specify import dependencies // This isn't included in the build, but is read by qmlimportscanner for builds. import QtQuick 2.2 import QtQuick.PrivateWidgets 1.1 Item { } ================================================ FILE: src/qml/extension-server/ExtensionServerSettings.qml ================================================ import QtQuick 2.15 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.13 import QtQuick.Controls 1.4 as LC import Qt.labs.settings 1.0 import QtQuick.Window 2.3 import "../common" import "../settings" import "../common/platformutils.js" as PlatformUtils BetterDialog { id: root title: qsTranslate("RESP","Extension Server") footer: null property bool restartRequired: false contentItem: Rectangle { id: dialogRoot implicitWidth: 950 implicitHeight: 550 color: sysPalette.base Control { palette: approot.palette anchors.fill: parent anchors.margins: 20 ScrollView { id: globalSettingsScrollView width: parent.width height: parent.height ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ColumnLayout { id: innerLayout width: globalSettingsScrollView.width - 25 height: (dialogRoot.height - 50 > implicitHeight) ? dialogRoot.height - 50 : implicitHeight spacing: 10 RowLayout { Layout.fillWidth: true SettingsGroupTitle { Layout.fillWidth: true text: qsTranslate("RESP","Connection Settings") } } ColumnLayout { Layout.fillWidth: true spacing: 10 RowLayout { BetterLabel { Layout.preferredWidth: 200 text: qsTranslate("RESP","Server Url:") } BetterTextField { id: serverUrl Layout.fillWidth: true } } RowLayout { BetterLabel { Layout.preferredWidth: 200 text: qsTranslate("RESP","Basic Auth:") } BetterTextField { id: serverAuthTokenName Layout.fillWidth: true placeholderText: qsTranslate("RESP","User") } PasswordInput { id: serverAuthTokenValue Layout.fillWidth: true placeholderText: qsTranslate("RESP","Password") } } IntOption { id: responseTimeout Layout.preferredHeight: 30 Layout.fillWidth: true min: 1 max: 60 value: 10 label: qsTranslate("RESP","Response timeout (in seconds)") } } RowLayout { Layout.topMargin: 10 SettingsGroupTitle { text: qsTranslate("RESP", "Available Data Formatters") } Item { Layout.fillWidth: true } BetterButton { text: qsTranslate("RESP", "Reload") onClicked: { formattersManager.loadFormatters(); } } } LC.TableView { id: formattersTable Layout.fillWidth: true Layout.fillHeight: true Layout.preferredHeight: 100 verticalScrollBarPolicy: Qt.ScrollBarAlwaysOn LC.TableViewColumn { role: "id" width: 75 title: qsTranslate("RESP","Id") } LC.TableViewColumn { width: 250 role: "name" title: qsTranslate("RESP","Name") } LC.TableViewColumn { width: 75 role: "readOnly" title: qsTranslate("RESP","Read Only") } model: formattersManager } Item { visible: !formattersTable.visible Layout.fillHeight: true } RowLayout { Layout.fillWidth: true Item { Layout.fillWidth: true; } BetterButton { text: qsTranslate("RESP","OK") onClicked: { if (root.restartRequired === true) { // restart app Qt.exit(1001) } restartRequired = false root.close() } } BetterButton { text: qsTranslate("RESP","Cancel") onClicked: root.close() } } } } } } Settings { id: globalSettings category: "app" property alias extensionServerUrl: serverUrl.text property alias extensionServerUser: serverAuthTokenName.text property alias extensionServerPassword: serverAuthTokenValue.text property alias extensionServerRequestTimeout: responseTimeout.value } Component.onCompleted: { restartRequired = false } } ================================================ FILE: src/qml/qml.qrc ================================================ app.qml WelcomeTab.qml QuickStartDialog.qml common/RichTextWithLinks.qml common/PasswordInput.qml common/AddressInput.qml common/FilePathInput.qml common/BetterTabView.qml common/BetterTab.qml connections/ConnectionSettignsDialog.qml connections-tree/menu/InlineMenu.qml connections-tree/menu/server.qml connections-tree/menu/database.qml connections-tree/menu/namespace.qml connections-tree/menu/key.qml value-editor/AddKeyDialog.qml value-editor/ValueTabs.qml value-editor/Pagination.qml value-editor/editors/HashItemEditor.qml value-editor/editors/StreamItemEditor.qml value-editor/editors/SingleItemEditor.qml value-editor/editors/SortedSetItemEditor.qml value-editor/editors/AbstractEditor.qml value-editor/editors/MultilineEditor.qml value-editor/editors/formatters/hexy.js value-editor/editors/editor.js console/RedisConsole.qml console/BaseConsole.qml console/Consoles.qml connections-tree/BetterTreeView.qml AppToolBar.qml common/BetterSplitView.qml common/ImageButton.qml settings/GlobalSettings.qml settings/BoolOption.qml settings/FontSizeOption.qml settings/IntOption.qml bulk-operations/BulkOperationsDialog.qml settings/ComboboxOption.qml common/platformutils.js common/NewTextArea.qml common/BetterGroupbox.qml common/BetterCheckbox.qml common/BetterRadioButton.qml common/BetterTextField.qml common/SettingsGroupTitle.qml common/BetterButton.qml LogView.qml value-editor/ValueTableCell.qml common/BetterTabButton.qml common/BetterMessageDialog.qml common/BetterDialog.qml common/OkDialog.qml common/OkDialogOverlay.qml common/BetterSpinBox.qml common/BetterComboBox.qml value-editor/editors/formatters/ValueFormatters.qml common/FastTextView.qml common/BetterDialogButtonBox.qml common/BetterToolTip.qml common/SaveToFileButton.qml common/BetterLabel.qml connections-tree/ConnectionGroupDialog.qml connections-tree/menu/server_group.qml value-editor/ValueTable.qml value-editor/ValueTableActions.qml value-editor/filters/ListFilters.qml value-editor/filters/StreamFilters.qml common/JsonHighlighter.qml connections/AskSecretDialog.qml common/ColorInput.qml value-editor/editors/UnsupportedDataType.qml value-editor/editors/ReadOnlySingleItemEditor.qml extension-server/ExtensionServerSettings.qml connections-tree/TreeItemDelegate.qml server-actions/ServerActionTabs.qml server-actions/ServerCharts.qml server-actions/ServerConfig.qml server-actions/ServerSlowlog.qml server-actions/ServerClients.qml server-actions/ServerPubSub.qml server-actions/ServerAction.qml common/LegacyTableView.qml common/BetterMenu.qml common/BetterMenuItem.qml ================================================ FILE: src/qml/server-actions/ServerAction.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 2.13 Item { id: root property var model property alias uiBlocked: uiBlocker.visible function stopTimer() {} Component.onCompleted: { uiBlocker.visible = true } Rectangle { id: uiBlocker visible: false anchors.fill: parent color: Qt.rgba(sysPalette.base.red, sysPalette.base.green, sysPalette.base.blue, 0.15) z: 1000 Item { anchors.fill: parent ProgressBar { anchors.centerIn: parent indeterminate: true } } MouseArea { anchors.fill: parent } } } ================================================ FILE: src/qml/server-actions/ServerActionTabs.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.13 import QtQuick.Controls 2.13 import QtQuick.Controls 1.4 as LC import QtQuick.Window 2.2 import QtCharts 2.3 import "./../common" import "./../common/platformutils.js" as PlatformUtils import "./../settings" Repeater { id: root BetterTab { id: serverTab Component { id: serverTabButton BetterTabButton { icon.source: PlatformUtils.getThemeIcon("database.svg") text: tabName onCloseClicked: { serverStatsModel.closeTab(tabIndex) } } } Component.onCompleted: { var tabButton = serverTabButton.createObject(serverTab); tabButton.self = tabButton; tabButton.tabRef = serverTab; tabBar.addItem(tabButton) tabBar.activateTabButton(tabButton) tabs.activateTab(serverTab) } property var model: tabModel onModelChanged: { if (!model) return; serverTab.model.init() } function getValue(cat, prop) { try { return model.serverInfo[cat][prop] } catch(e) { console.error("Cannot get server info '" + prop + "' from " + cat) return "" } } function getIntValue(cat, prop) { var val = getValue(cat, prop) if (val !== "") return parseInt(val) return 0 } function getHitRatio() { var hits = getIntValue("stats", "keyspace_hits") var misses = getIntValue("stats", "keyspace_misses") var total = hits + misses if (total === 0) { return 0 } return Math.round(hits / total * 100 * 100) / 100 } Rectangle { id: wrappingBackground anchors.fill: parent color: sysPalette.base ColumnLayout { clip: true anchors.fill: parent anchors.margins: 15 Component { id: actionsMenu ColumnLayout { GridLayout { id: tileGrid columns: 4 Layout.fillWidth: true property int tileSize: PlatformUtils.isScalingDisabled()? 150 : 110 property int tileIconSize: PlatformUtils.isScalingDisabled()? 90 : 75 ImageButton { objectName: "rdm_server_action_info" Layout.fillWidth: true Layout.rowSpan: 2 implicitHeight: tileGrid.tileSize * 2 tooltip: qsTranslate("RESP", "View Server Info") showBorder: true imgStickTop: true imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: PlatformUtils.getThemeIcon("server-config.svg") onClicked: { currentAction.text = tooltip serverStackView.push(serverConfig) } GridLayout { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.margins: 15 columns: 2 flow: GridLayout.LeftToRight Text { text: qsTranslate("RESP","Redis Version") font.pointSize: 12 color: sysPalette.windowText } BetterLabel { id: redisVersionLabel text: "N/A" font.pointSize: 12 objectName: "rdm_server_info_redis_version" } Text { text: qsTranslate("RESP","Uptime") font.pointSize: 12 color: sysPalette.windowText } BetterLabel { id: uptimeLabel; text: "N/A"; font.pointSize: 12 objectName: "rdm_server_info_uptime" } Text { text: qsTranslate("RESP","Hit Ratio") font.pointSize: 12 color: sysPalette.windowText } BetterLabel { id: hitRatioLabel; text: "N/A"; font.pointSize: 12 objectName: "rdm_server_info_hit_ratio" } Text { text: qsTranslate("RESP","Used memory") font.pointSize: 12 color: sysPalette.windowText } BetterLabel { id: usedMemoryLabel; text: "N/A"; font.pointSize: 12 objectName: "rdm_server_info_used_memory" } Text { text: qsTranslate("RESP","Cmd Processed") font.pointSize: 12 color: sysPalette.windowText wrapMode: Text.WordWrap } BetterLabel { id: totalCommandsProcessedLabel; text: "N/A"; font.pointSize: 12 objectName: "rdm_server_info_cmd_processed" } } } ImageButton { objectName: "rdm_server_action_monitor" Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "Monitor Commands") showBorder: true imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: PlatformUtils.getThemeIcon("console.svg") onClicked: { serverTab.model.monitorCommands() } } ImageButton { objectName: "rdm_server_action_slowlog" Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "Slowlog") showBorder: true imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: PlatformUtils.getThemeIcon("slowlog.svg") onClicked: { currentAction.text = text serverTab.model.refreshSlowLog = true serverStackView.push(serverSlowlog) } } ImageButton { id: connectedClientsBtn objectName: "rdm_server_action_clients" Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "Clients") showBorder: true imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: PlatformUtils.getThemeIcon("clients.svg") onClicked: { currentAction.text = text serverTab.model.refreshClients = true serverStackView.push(serverClients) } } ImageButton { objectName: "rdm_server_action_charts" Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "Server Stats") showBorder: true imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: PlatformUtils.getThemeIcon("server-stats.svg") onClicked: { currentAction.text = text serverStackView.push(serverCharts) } } ImageButton { objectName: "rdm_server_action_console" Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "Console") showBorder: true imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: PlatformUtils.getThemeIcon("console.svg") onClicked: { serverTab.model.openTerminal() } } ImageButton { objectName: "rdm_server_action_pubsub" Layout.fillWidth: true implicitHeight: tileGrid.tileSize text: qsTranslate("RESP", "Pub/Sub Channels") showBorder: true imgWidth: tileGrid.tileIconSize imgHeight: tileGrid.tileIconSize iconSource: PlatformUtils.getThemeIcon("pub-sub-channels.svg") onClicked: { currentAction.text = text serverTab.model.refreshPubSubMonitor = true serverStackView.push(serverPubSub) } } Connections { target: model? model : null function onServerInfoChanged() { usedMemoryLabel.text = serverTab.getValue("memory", "used_memory_human") redisVersionLabel.text = serverTab.getValue("server", "redis_version") connectedClientsBtn.text = qsTranslate("RESP", "Clients") + " " + serverTab.getValue("clients", "connected_clients") totalCommandsProcessedLabel.text = serverTab.getValue("stats", "total_commands_processed") uptimeLabel.text = serverTab.getValue("server", "uptime_in_days") + qsTranslate("RESP"," day(s)") hitRatioLabel.text = serverTab.getHitRatio() + "%" } } } Item { Layout.fillHeight: true } } } Component { id: serverCharts ServerCharts { model: serverTab.model } } Component { id: serverClients ServerClients { model: serverTab.model } } Component { id: serverConfig ServerConfig { model: serverTab.model } } Component { id: serverPubSub ServerPubSub { model: serverTab.model } } Component { id: serverSlowlog ServerSlowlog { model: serverTab.model } } RowLayout { visible: serverStackView.depth > 1 BetterButton { text: qsTr("Server Actions") onClicked: { serverStackView.currentItem.stopTimer() serverStackView.pop() } } BetterLabel { text: "❯" } BetterLabel {id: currentAction} } StackView { id: serverStackView Layout.fillHeight: true Layout.fillWidth: true initialItem: actionsMenu } } } } } ================================================ FILE: src/qml/server-actions/ServerCharts.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.13 import QtQuick.Controls 2.13 import QtQuick.Controls 1.4 as LC import QtQuick.Window 2.2 import QtCharts 2.3 import "./../common" import "./../common/platformutils.js" as PlatformUtils import "./../settings" ServerAction { id: infoCharts clip: true ScrollView { width: parent.width height: parent.height ScrollBar.horizontal.policy: ScrollBar.AlwaysOff GridLayout { id: infoChartsGrid columns: 2 columnSpacing: 0 rowSpacing: 20 width: infoCharts.width - 25 height: implicitHeight property int chartWidth: infoCharts.width / 2 property int chartHeight: (infoCharts.height - rowSpacing) / 2 function chartTheme() { if (sysPalette.base.hslLightness < 0.4) { return ChartView.ChartThemeDark } else { return ChartView.ChartThemeLight } } ChartView { id: chartCommandsPerSec Layout.preferredWidth: parent.chartWidth Layout.preferredHeight: parent.chartHeight legend.visible: false backgroundRoundness: 0 theme: parent.chartTheme() backgroundColor: sysPalette.base title: qsTranslate("RESP","Commands Per Second") antialiasing: true DateTimeAxis { id: axisXCommandsPerSec min: new Date() format: "HH:mm:ss" } ValueAxis { id: axisYCommandsPerSec min: 0 max: 100 labelFormat: "%d" titleText: qsTranslate("RESP","Ops/s") } LineSeries { id: commands_per_sec_series name: "commands_per_sec" axisX: axisXCommandsPerSec axisY: axisYCommandsPerSec } } ChartView { id: chartConnectedClients Layout.preferredWidth: parent.chartWidth Layout.preferredHeight: parent.chartHeight legend.visible: false backgroundRoundness: 0 theme: parent.chartTheme() backgroundColor: sysPalette.base title: qsTranslate("RESP","Connected Clients") antialiasing: true DateTimeAxis { id: axisXConnectedClients min: new Date() format: "HH:mm:ss" } ValueAxis { id: axisYConnectedClients min: 0 max: 100 labelFormat: "%d" titleText: qsTranslate("RESP","Clients") } SplineSeries { id: connected_clients_series name: "connected_clients" axisX: axisXConnectedClients axisY: axisYConnectedClients } } ChartView { id: chartMemoryUsage objectName: "rdm_server_info_tab_memory_usage" Layout.preferredWidth: parent.chartWidth Layout.preferredHeight: parent.chartHeight legend.visible: false backgroundRoundness: 0 theme: parent.chartTheme() backgroundColor: sysPalette.base title: qsTranslate("RESP","Memory Usage") antialiasing: true DateTimeAxis { id: axisXMemoryUsage min: new Date() format: "HH:mm:ss" } ValueAxis { id: axisYMemoryUsage min: 0 titleText: qsTranslate("RESP","Mb") } function toMsecsSinceEpoch(date) { var msecs = date.getTime(); return msecs; } SplineSeries { id: used_memory_series name: "used_memory" axisX: axisXMemoryUsage axisY: axisYMemoryUsage } } ChartView { id: chartNetworkInput Layout.preferredWidth: parent.chartWidth Layout.preferredHeight: parent.chartHeight legend.visible: false backgroundRoundness: 0 theme: parent.chartTheme() backgroundColor: sysPalette.base title: qsTranslate("RESP","Network Input") antialiasing: true DateTimeAxis { id: axisXNetworkInput min: new Date() format: "HH:mm:ss" } ValueAxis { id: axisYNetworkInput min: 0 titleText: qsTranslate("RESP","Kb/s") } LineSeries { id: network_input_series name: "network_input" axisX: axisXNetworkInput axisY: axisYNetworkInput } } ChartView { id: chartNetworkOutput Layout.preferredWidth: parent.chartWidth Layout.preferredHeight: parent.chartHeight legend.visible: false backgroundRoundness: 0 theme: parent.chartTheme() backgroundColor: sysPalette.base title: qsTranslate("RESP","Network Output") antialiasing: true DateTimeAxis { id: axisXNetworkOutput min: new Date() format: "HH:mm:ss" } ValueAxis { id: axisYNetworkOutput min: 0 titleText: qsTranslate("RESP","Kb/s") } LineSeries { id: network_output_series name: "network_output" axisX: axisXNetworkOutput axisY: axisYNetworkOutput } } ChartView { id: chartTotalKeys Layout.preferredWidth: parent.chartWidth Layout.preferredHeight: parent.chartHeight legend.visible: false backgroundRoundness: 0 theme: parent.chartTheme() backgroundColor: sysPalette.base title: qsTranslate("RESP","Total Error Replies") antialiasing: true DateTimeAxis { id: axisXTotalErrors min: new Date() format: "HH:mm:ss" } ValueAxis { id: axisYTotalErrors min: 0 max: 100 labelFormat: "%d" titleText: qsTranslate("RESP","Error Replies") } LineSeries { id: total_errors_series name: "total_errors" axisX: axisXTotalErrors axisY: axisYTotalErrors } } } } Connections { target: infoCharts.model? infoCharts.model : null function onServerInfoChanged() { if (uiBlocked) { uiBlocked = false } var getUsedMemory = function (name) { return Math.round(parseFloat(infoCharts.model.serverInfo["memory"][name] ) / (1024 * 1024) * 100) / 100; } // Commands per second var commandsPerSec = parseInt(serverTab.getValue("stats", "instantaneous_ops_per_sec")) var commandsPerSecMax = commandsPerSec + (10 - commandsPerSec % 10) if (commandsPerSecMax > axisYCommandsPerSec.max) axisYCommandsPerSec.max = commandsPerSecMax qmlUtils.addNewValueToDynamicChart(commands_per_sec_series, commandsPerSec) // Connected clients var connectedClients = parseInt(serverTab.getValue("clients", "connected_clients")) var connectedClientsMax = connectedClients + (10 - connectedClients % 10) if (connectedClientsMax > axisYConnectedClients.max) axisYConnectedClients.max = connectedClientsMax qmlUtils.addNewValueToDynamicChart(connected_clients_series, connectedClients) // Memory usage var usedMemory = getUsedMemory("used_memory") var memoryUsageMax = getUsedMemory("used_memory") + (10 - usedMemory % 10) if (memoryUsageMax > axisYMemoryUsage.max) axisYMemoryUsage.max = memoryUsageMax qmlUtils.addNewValueToDynamicChart(used_memory_series, usedMemory) // Network input var networkInput = parseFloat(serverTab.getValue("stats", "instantaneous_input_kbps")) var networkInputMax = networkInput + (10 - networkInput % 10) if (networkInputMax > axisYNetworkInput.max) axisYNetworkInput.max = networkInputMax qmlUtils.addNewValueToDynamicChart(network_input_series, networkInput) // Network output var networkOutput = parseFloat(serverTab.getValue("stats", "instantaneous_output_kbps")) var networkOutputMax = networkOutput + (10 - networkOutput % 10) if (networkOutputMax > axisYNetworkOutput.max) axisYNetworkOutput.max = networkOutputMax qmlUtils.addNewValueToDynamicChart(network_output_series, networkOutput) // Total errors var totalErrors = parseInt(serverTab.getValue("stats", "total_error_replies")) var totalErrorsMax = totalErrors + (10 - totalErrors % 10) if (totalErrorsMax > axisYTotalErrors.max) axisYTotalErrors.max = totalErrorsMax qmlUtils.addNewValueToDynamicChart(total_errors_series, totalErrors) } } } ================================================ FILE: src/qml/server-actions/ServerClients.qml ================================================ import QtQuick 2.15 import QtQuick.Layouts 1.13 import QtQuick.Controls 2.13 import QtQuick.Controls 1.4 as LC import Qt.labs.qmlmodels 1.0 import QtQuick.Window 2.2 import QtCharts 2.3 import "./../common" import "./../common/platformutils.js" as PlatformUtils import "./../settings" import "./../value-editor" ServerAction { id: tab Connections { target: tab.model? tab.model : null function onClientsChanged() { if (uiBlocked) { uiBlocked = false } } } ColumnLayout { anchors.fill: parent anchors.margins: 10 BoolOption { Layout.preferredWidth: 200 Layout.preferredHeight: 40 value: true label: qsTranslate("RESP","Auto Refresh") onValueChanged: { tab.model.refreshClients = value } } LegacyTableView { Layout.fillHeight: true Layout.fillWidth: true model: tab.model.clients ? tab.model.clients : [] LC.TableViewColumn { role: "addr" title: qsTranslate("RESP","Client Address") width: 200 } LC.TableViewColumn { role: "age" title: qsTranslate("RESP","Age (sec)") width: 75 } LC.TableViewColumn { role: "idle" title: qsTranslate("RESP","Idle") width: 75 } LC.TableViewColumn { role: "flags" title: qsTranslate("RESP","Flags") width: 75 } LC.TableViewColumn { role: "db" title: qsTranslate("RESP","Current Database") width: 120 } } } } ================================================ FILE: src/qml/server-actions/ServerConfig.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.13 import QtQuick.Controls 2.13 import QtQuick.Controls 1.4 as LC import QtQuick.Window 2.2 import QtCharts 2.3 import "./../common" import "./../common/platformutils.js" as PlatformUtils import "./../settings" ServerAction { id: tab uiBlocked: !serverInfoBuilder.model ColumnLayout { anchors.fill: parent anchors.margins: 10 BoolOption { id: autorefreshSwitch Layout.preferredWidth: 200 Layout.preferredHeight: 40 value: true label: qsTranslate("RESP","Auto Refresh") } TabBar { id: serverInfoDetailsTabBar Layout.fillWidth: true Layout.preferredHeight: 30 visible: !uiBlocked currentIndex: 0 Repeater { id: serverInfoBuilderTabButtons TabButton { text: modelData['name'] implicitWidth: 100 } } } StackLayout { id: serverInfoTabs Layout.fillWidth: true Layout.fillHeight: true Layout.topMargin: 15 currentIndex: serverInfoDetailsTabBar.currentIndex Repeater { id: serverInfoBuilder LegacyTableView { model: modelData['section_data'] LC.TableViewColumn { role: "name" title: qsTranslate("RESP","Property") width: 250 } LC.TableViewColumn { role: "value" title: qsTranslate("RESP","Value") width: 350 } } } Connections { target: tab.model? tab.model : null function onServerInfoChanged() { if (autorefreshSwitch.value === false) return; loadServerInfo(); if (uiBlocked) uiBlocked = false; } function loadServerInfo() { var sections = [] for (var section in tab.model.serverInfo) { var section_data = [] for (var key in tab.model.serverInfo[section]) { var property = {"name": key, "value": tab.model.serverInfo[section][key]} section_data.push(property) } sections.push({"name": section, "section_data": section_data}) } var currentTab = serverInfoTabs.currentIndex serverInfoBuilder.model = sections serverInfoBuilderTabButtons.model = sections serverInfoDetailsTabBar.currentIndex = currentTab } } } } } ================================================ FILE: src/qml/server-actions/ServerPubSub.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.13 import QtQuick.Controls 2.13 import QtQuick.Controls 1.4 as LC import QtQuick.Window 2.2 import QtCharts 2.3 import "./../common" import "./../common/platformutils.js" as PlatformUtils import "./../settings" ServerAction { id: tab function stopTimer() { model.refreshPubSubMonitor = false } Connections { target: tab.model? tab.model : null function onPubSubChannelsChanged() { if (uiBlocked) { uiBlocked = false } } } ColumnLayout { anchors.fill: parent anchors.margins: 10 BoolOption { Layout.preferredWidth: 200 Layout.preferredHeight: 40 value: true label: qsTranslate("RESP","Enable") onValueChanged: { tab.model.refreshPubSubMonitor = value } } LegacyTableView { Layout.fillHeight: true Layout.fillWidth: true model: tab.model.pubSubChannels ? tab.model.pubSubChannels : [] rowDelegate: Item { height: 50 } LC.TableViewColumn { role: "addr" title: qsTranslate("RESP","Channel Name") width: 200 } LC.TableViewColumn { role: "addr" width: 200 delegate: Item { BetterButton { objectName: "rdm_server_info_pub_sub_subscribe_to_channel_btn" anchors.centerIn: parent text: qsTranslate("RESP","Subscribe in Console") onClicked: { console.log(styleData.value) tab.model.subscribeToChannel(styleData.value) } } } } } } } ================================================ FILE: src/qml/server-actions/ServerSlowlog.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.13 import QtQuick.Controls 2.13 import QtQuick.Controls 1.4 as LC import QtQuick.Window 2.2 import QtCharts 2.3 import "./../common" import "./../common/platformutils.js" as PlatformUtils import "./../settings" ServerAction { id: tab function stopTimer() { model.refreshSlowLog = false } Connections { target: tab.model? tab.model : null function onSlowLogChanged() { if (uiBlocked) { uiBlocked = false } } } ColumnLayout { anchors.fill: parent anchors.margins: 10 BoolOption { Layout.preferredWidth: 200 Layout.preferredHeight: 40 value: true label: qsTranslate("RESP","Auto Refresh") onValueChanged: { tab.model.refreshSlowLog = value } } LegacyTableView { Layout.fillHeight: true Layout.fillWidth: true model: tab.model.slowLog ? tab.model.slowLog : [] LC.TableViewColumn { role: "cmd" title: qsTranslate("RESP","Command") width: 600 delegate: BetterLabel { text: { var result = ""; for (var index in modelData['cmd']) { result += modelData['cmd'][index] + " "; } return result; } elide: styleData.elideMode } } LC.TableViewColumn { role: "time" title: qsTranslate("RESP","Processed at") width: 150 delegate: BetterLabel { text: { return new Date(modelData['time']*1000).toLocaleString( locale, PlatformUtils.dateTimeFormat); } elide: styleData.elideMode } } LC.TableViewColumn { role: "exec_time" title: qsTranslate("RESP","Execution Time (μs)") width: 150 } } } } ================================================ FILE: src/qml/settings/BoolOption.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 import "../common" Item { id: root property string label property string description property bool value onValueChanged: { if (val.checked != root.value) { val.checked = root.value } } signal clicked RowLayout { anchors.fill: parent ColumnLayout { Layout.fillWidth: true spacing: 1 BetterLabel { Layout.fillWidth: true text: root.label } Text { color: disabledSysPalette.text text: root.description visible: root.description } } Switch { id: val rightPadding: 0 indicator: Rectangle { implicitWidth: 48 implicitHeight: 26 x: val.leftPadding y: parent.height / 2 - height / 2 radius: 13 color: val.checked ? sysPalette.highlight : sysPalette.button border.color: val.checked ? sysPalette.highlight : sysPalette.button Rectangle { x: val.checked ? parent.width - width : 0 width: 26 height: 26 radius: 13 color: val.checked ? sysPalette.midlight : sysPalette.mid border.color: val.checked ? sysPalette.highlight : sysPalette.button } } onCheckedChanged: { root.value = checked } onClicked: { root.clicked() } } } } ================================================ FILE: src/qml/settings/ComboboxOption.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.2 import "../common" Item { id: root property string label property string description property string value property alias model: val.model property int popupMaxHeight: 350 property int popupWidth: val.width onValueChanged: { if (val.currentText != root.value) { val.currentIndex = val.find(root.value) } } RowLayout { anchors.fill: parent ColumnLayout { Layout.fillWidth: true spacing: 1 BetterLabel { Layout.fillWidth: true Layout.fillHeight: true text: root.label } Text { color: "grey" text: root.description visible: !!text } } BetterComboBox { id: val Layout.minimumWidth: 80 onActivated: { if (index >= 0) root.value = textAt(index) } onCountChanged: { if (model) { currentIndex = val.find(root.value) } } Component.onCompleted: { if (model) { currentIndex = val.find(root.value) } popup.contentItem.implicitHeight = Qt.binding(function () { return Math.min(root.popupMaxHeight, val.popup.contentItem.contentHeight); }); popup.width = Qt.binding(function () { return Math.max(root.popupWidth, val.popup.contentItem.contentWidth); }); } } } } ================================================ FILE: src/qml/settings/FontSizeOption.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.2 import "../common" import "." ComboboxOption { id: root property int minFontSize: 4 property int maxFontSize: 16 Component.onCompleted: { var m = [] for (var c=minFontSize; c <= maxFontSize; c++) { m.push(c) } model = m; } } ================================================ FILE: src/qml/settings/GlobalSettings.qml ================================================ import QtQuick 2.15 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.13 import QtQuick.Controls 1.4 as LC import Qt.labs.settings 1.0 import QtQuick.Window 2.3 import "../common" import "." import "../common/platformutils.js" as PlatformUtils BetterDialog { id: root title: qsTranslate("RESP","Settings") footer: null property bool restartRequired: false contentItem: Rectangle { id: dialogRoot implicitWidth: PlatformUtils.isScalingDisabled() ? 1100 : 950 implicitHeight: PlatformUtils.isScalingDisabled() ? 700 : 550 color: sysPalette.base Control { palette: approot.palette anchors.fill: parent anchors.margins: 20 ScrollView { id: globalSettingsScrollView width: parent.width height: parent.height ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ColumnLayout { id: innerLayout width: globalSettingsScrollView.width - 25 height: (dialogRoot.height - 50 > implicitHeight) ? dialogRoot.height - 50 : implicitHeight spacing: PlatformUtils.isScalingDisabled()? 20 : 10 RowLayout { Layout.fillWidth: true SettingsGroupTitle { Layout.fillWidth: true text: qsTranslate("RESP","General") } BetterLabel { color: disabledSysPalette.text text: qsTranslate("RESP","Application will be restarted to apply these settings.") } } GridLayout { columns: 2 rows: 3 flow: GridLayout.TopToBottom Layout.fillWidth: true rowSpacing: PlatformUtils.isScalingDisabled() ? 20 : 10 columnSpacing: PlatformUtils.isScalingDisabled() ? 20 : 15 ComboboxOption { id: appLang Layout.fillWidth: true Layout.preferredHeight: 30 model: ["system", "en_US", "zh_CN", "zh_TW", "uk_UA", "es_ES", "ja_JP"] value: "system" label: qsTranslate("RESP","Language") onValueChanged: root.restartRequired = true } ComboboxOption { id: appFont Layout.fillWidth: true Layout.preferredHeight: 30 popupWidth: 300 model: Qt.fontFamilies() label: qsTranslate("RESP","Font") onValueChanged: root.restartRequired = true } FontSizeOption { id: appFontSize Layout.fillWidth: true Layout.preferredHeight: 30 label: qsTranslate("RESP","Font Size") onValueChanged: root.restartRequired = true } ComboboxOption { id: darkModeWindows Layout.fillWidth: true Layout.preferredHeight: 30 model: ["Auto", "On", "Off"] value: "Auto" label: qsTranslate("RESP","Dark Mode") visible: PlatformUtils.isWindows() onValueChanged: root.restartRequired = true } BoolOption { id: darkModeLinux Layout.fillWidth: true Layout.preferredHeight: 30 value: false label: qsTranslate("RESP","Dark Mode") visible: PlatformUtils.isLinux() onValueChanged: root.restartRequired = true } BoolOption { id: systemProxy Layout.fillWidth: true Layout.preferredHeight: 30 value: false label: qsTranslate("RESP","Use system proxy settings") onValueChanged: root.restartRequired = true } BoolOption { id: disableProxyForRedisConnections Layout.fillWidth: true Layout.preferredHeight: 30 value: false label: qsTranslate("RESP","Use system proxy only for HTTP(S) requests") } } SettingsGroupTitle { Layout.topMargin: 10 text: qsTranslate("RESP","Value Editor") } GridLayout { columns: 2 rows: 2 flow: GridLayout.TopToBottom rowSpacing: PlatformUtils.isScalingDisabled() ? 20 : 10 columnSpacing: PlatformUtils.isScalingDisabled() ? 20 : 15 ComboboxOption { id: valueEditorFont Layout.fillWidth: true Layout.preferredHeight: 30 popupWidth: 300 model: Qt.fontFamilies() label: qsTranslate("RESP","Font") onValueChanged: root.restartRequired = true } FontSizeOption { id: valueEditorFontSize Layout.fillWidth: true Layout.preferredHeight: 30 value: Qt.platform.os == "osx"? "12" : "11" label: qsTranslate("RESP","Font Size") onValueChanged: root.restartRequired = true } IntOption { id: valueSizeLimit Layout.fillWidth: true Layout.preferredHeight: 30 min: 1000 max: 20000000 value: 1500000 label: qsTranslate("RESP","Maximum Formatted Value Size") description: qsTranslate("RESP", "Size in bytes") } IntOption { id: valueEditorPageSizeControl Layout.fillWidth: true Layout.preferredHeight: 30 min: 10 max: 10000 value: 100 label: qsTranslate("RESP","Maximum amount of items per page") } } SettingsGroupTitle { text: qsTranslate("RESP","Connections Tree") Layout.topMargin: 20 } GridLayout { columns: 2 rows: 4 flow: GridLayout.TopToBottom rowSpacing: PlatformUtils.isScalingDisabled() ? 20 : 10 columnSpacing: PlatformUtils.isScalingDisabled() ? 20 : 15 BoolOption { id: nsOnTop Layout.fillWidth: true Layout.preferredHeight: 30 value: Qt.platform.os == "windows"? true : false label: qsTranslate("RESP","Show namespaced keys on top") } BoolOption { id: nsReload Layout.fillWidth: true Layout.preferredHeight: 30 value: true label: qsTranslate("RESP","Reopen namespaces on reload") description: qsTranslate("RESP","(Disable to improve treeview performance)") } BoolOption { id: namespacedKeysShortName Layout.fillWidth: true Layout.preferredHeight: 30 Layout.rowSpan: 2 value: true label: qsTranslate("RESP","Show only last part for namespaced keys") } IntOption { id: scanCommandLimit Layout.fillWidth: true Layout.preferredHeight: 30 min: 1000 max: 500000 value: 10000 label: qsTranslate("RESP","Limit for SCAN command") } IntOption { id: childItemsLimit Layout.fillWidth: true Layout.preferredHeight: 30 min: 1 max: 100000 value: 1000 label: qsTranslate("RESP","Maximum amount of rendered child items") } IntOption { id: liveKeyLimit Layout.fillWidth: true Layout.preferredHeight: 30 min: 100 max: 100000 value: 1000 label: qsTranslate("RESP","Live update maximum allowed keys") } IntOption { id: liveUpdateInterval Layout.fillWidth: true Layout.preferredHeight: 30 min: 3 max: 100000 value: 10 label: qsTranslate("RESP","Live update interval (in seconds)") } } Item { Layout.fillHeight: true } RowLayout { Layout.fillWidth: true Item { Layout.fillWidth: true; } BetterButton { text: qsTranslate("RESP","OK") onClicked: { if (!PlatformUtils.isOSX() && root.restartRequired === true) { // restart app Qt.exit(1001) } restartRequired = false root.close() } } BetterButton { text: qsTranslate("RESP","Cancel") onClicked: root.close() } } } } } } Settings { id: globalSettings category: "app" property alias showNamespacesOnTop: nsOnTop.value property alias reopenNamespacesOnReload: nsReload.value property alias namespacedKeysShortName: namespacedKeysShortName.value property alias treeItemMaxChilds: childItemsLimit.value property alias liveUpdateKeysLimit: liveKeyLimit.value property alias liveUpdateInterval: liveUpdateInterval.value property alias appFont: appFont.value property alias appFontSize: appFontSize.value property alias valueEditorFont: valueEditorFont.value property alias valueEditorFontSize: valueEditorFontSize.value property alias valueSizeLimit: valueSizeLimit.value property alias valueEditorPageSize: valueEditorPageSizeControl.value property alias locale: appLang.value property alias darkModeOn: darkModeLinux.value property alias darkMode: darkModeWindows.value property alias useSystemProxy: systemProxy.value property alias disableProxyForRedisConnections: disableProxyForRedisConnections.value property alias scanLimit: scanCommandLimit.value } Settings { id: customFormatters category: "formatters" property var formatters } Component.onCompleted: { restartRequired = false } } ================================================ FILE: src/qml/settings/IntOption.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.3 import "./../common" Item { id: root property string label property string description property int value property alias min: val.from property alias max: val.to onValueChanged: { if (val.value != root.value) { val.value = root.value } } RowLayout { anchors.fill: parent ColumnLayout { Layout.fillWidth: true spacing: 1 BetterLabel { Layout.fillWidth: true Layout.fillHeight: !root.description text: root.label } Text { color: disabledSysPalette.text text: root.description visible: root.description } } BetterSpinBox { id: val Layout.minimumWidth: 80 onValueChanged: { root.value = value } } } } ================================================ FILE: src/qml/value-editor/AddKeyDialog.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.12 import "./../common" import "./editors/editor.js" as Editor import "../common/platformutils.js" as PlatformUtils BetterDialog { id: root title: qsTranslate("RESP","Add New Key to ") + (request? request.dbIdString: "") visible: false property var request property bool loadingKeyTypes: false property var supportedKeyTypes footer: null onRequestChanged: { if (!request) return; root.loadingKeyTypes = true; request.loadAdditionalKeyTypesInfo(processAdditionalKeyTypes); root.supportedKeyTypes = Editor.getSupportedKeyTypes(); } function processAdditionalKeyTypes() { console.log(root.supportedKeyTypes) if (arguments && arguments.length > 0) { for (var indx in arguments) { console.log("module:", arguments[indx]) root.supportedKeyTypes.push(arguments[indx]) } } console.log(root.supportedKeyTypes) root.supportedKeyTypes = supportedKeyTypes; root.loadingKeyTypes = false; } Item { anchors.fill: parent implicitHeight: PlatformUtils.isOSX() ? 400 : 600 implicitWidth: PlatformUtils.isOSX() ? 600 : 800 ColumnLayout { anchors.fill: parent anchors.margins: 5 BetterLabel { text: qsTranslate("RESP","Key:") } BetterTextField { id: newKeyName Layout.fillWidth: true objectName: "rdm_add_key_name_field" text: request? request.keyName : '' } BetterLabel { text: qsTranslate("RESP","Type:") } BetterComboBox { id: typeSelector model: root.supportedKeyTypes Layout.fillWidth: true objectName: "rdm_add_key_type_field" onCurrentIndexChanged: { if (valueAddEditor.item.keyType !== undefined) { valueAddEditor.item.keyType = typeSelector.model[typeSelector.currentIndex] } } BusyIndicator { anchors.centerIn: parent running: root.loadingKeyTypes === true width: typeSelector.height } } Loader { id: valueAddEditor Layout.fillWidth: true Layout.fillHeight: true Layout.preferredHeight: 300 asynchronous: true source: Editor.getEditorByTypeString( typeSelector.model[typeSelector.currentIndex], true) onLoaded: { item.state = "new" if (item.keyType !== undefined) item.keyType = typeSelector.model[typeSelector.currentIndex] item.initEmpty() } } BetterLabel { text: qsTranslate("RESP", "Or Import Value from the file") + ":" } FilePathInput { id: valueFilePath objectName: "rdm_add_key_value_file" Layout.fillWidth: true placeholderText: qsTranslate("RESP","(Optional) Any file") nameFilters: [ "Any file (*)" ] title: qsTranslate("RESP","Select file with value") path: "" } RowLayout { Layout.fillWidth: true Layout.minimumHeight: 40 Item { Layout.fillWidth: true } BetterButton { objectName: "rdm_add_key_save_btn" text: qsTranslate("RESP","Save") function submitNewKeyRequest(row) { root.request.keyName = newKeyName.text root.request.keyType = typeSelector.model[typeSelector.currentIndex] root.request.value = row root.request.valueFilePath = valueFilePath.path keyFactory.submitNewKeyRequest(root.request) } onClicked: { if (!valueAddEditor.item) return var validateVal = (valueFilePath.path === "") valueAddEditor.item.getValue(validateVal, function (valid, row) { if (!valid) return; submitNewKeyRequest(row); }) } Connections { target: keyFactory function onKeyAdded() { root.request = null valueAddEditor.item.reset() valueAddEditor.item.initEmpty() valueFilePath.path = "" root.close() } function onError(err) { addError.text = err addError.open() } } } BetterButton { text: qsTranslate("RESP","Cancel") onClicked: root.close() } } Item { Layout.fillWidth: true } } OkDialogOverlay { id: addError title: qsTranslate("RESP","Error") text: "" visible: false } } } ================================================ FILE: src/qml/value-editor/Pagination.qml ================================================ import QtQuick 2.3 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.13 import "../common" ColumnLayout { GridLayout { columns: 2 Layout.fillWidth: true BetterLabel { text: qsTranslate("RESP","Page") + ":" wrapMode: Text.WrapAnywhere } BetterTextField { id: pageField; text: table.currentPage; tooltip: qsTranslate("RESP", "Total pages: ") + table.totalPages Layout.fillWidth: true validator: IntValidator { locale: pageField.locale.name bottom: 1 top: table.totalPages } onFocusChanged: { if (focus) return; text = Qt.binding(function() { return table.currentPage; }); } onAccepted: { table.goToPage(text) } } BetterLabel { Layout.columnSpan: 2 text: qsTranslate("RESP","Size: ") + keyRowsCount } } RowLayout { Layout.maximumWidth: 200 Layout.fillWidth: true spacing: 1 BetterButton { Layout.fillWidth: true palette.buttonText: sysPalette.dark text: "❮" onClicked: table.goToPrevPage() } BetterButton { Layout.fillWidth: true palette.buttonText: sysPalette.dark text: "❯" onClicked: table.goToNextPage() objectName: "rdm_value_editor_next_page_button" } } } ================================================ FILE: src/qml/value-editor/ValueTable.qml ================================================ import QtQuick 2.13 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.13 import QtQuick.Controls.Styles 1.1 import QtQuick.Window 2.2 import "./editors/editor.js" as Editor import "./../common/platformutils.js" as PlatformUtils import "./../common" import "./filters" import rdm.models 1.0 import Qt.labs.qmlmodels 1.0 Item { id: root property var resizeGuide: null RowLayout { anchors.fill: parent ColumnLayout { id: tableLayout Layout.fillHeight: true Layout.minimumHeight: 100 RowLayout { Layout.minimumHeight: 15 Layout.preferredHeight: 30 Layout.fillWidth: true spacing: 1 Repeater { id: tableHeader model: keyTab.keyModel? keyTab.keyModel.columnNames : [] Rectangle { // Table header cell objectName: "rdm_value_tab_table_header_col" + index Layout.preferredHeight: 30 Layout.minimumWidth: { if (table.valueColumnWidthOverrides && table.valueColumnWidthOverrides[index] !== undefined) { return table.valueColumnWidthOverrides[index]; } if (index === 0) return table.firstColumnWidth else return table.valueColumnWidth } color: sysPalette.window BetterLabel { anchors.centerIn: parent text: { if (modelData === "rowNumber") { return "#"; } else { return modelData } } color: sysPalette.windowText } BetterLabel { // Sort indicator anchors.margins: 10 anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: "▲" color: sysPalette.mid visible: false } MouseArea { anchors.fill: parent onClicked: { var role = tableHeader.model[index] var order = (role == table.model.sortRole) ? 1 - table.model.sortOrder : Qt.AscendingOrder for (var i = 0; i < tableHeader.model.length; i++) { tableHeader.itemAt(i).children[1].visible = false } tableHeader.itemAt(index).children[1].text = (order === Qt.AscendingOrder) ? "▲" : "▼" tableHeader.itemAt(index).children[1].visible = true table.sort(role, order) } } Rectangle { id: resizeHandler visible: index > 0 && index < keyTab.keyModel.columnNames.length - 1 color: "transparent" height: parent.height implicitWidth: 5 anchors { top: parent.top topMargin: 2 bottom: parent.bottom bottomMargin: 2 right: parent.right } MouseArea { id: resizeMouseArea anchors.fill: parent cursorShape: Qt.SizeHorCursor hoverEnabled: true drag { target: resizeHandler minimumX: 20 smoothed: false } onPressed: { resizeHandler.anchors.right = undefined; if (root.resizeGuide !== null) { root.resizeGuide.destroy(); } var guide = resizeMarker.createObject(resizeHandler); root.resizeGuide = guide; guide.open(); } onReleased: { if (resizeHandler.x > 0) { table.setColumnWidth(index, resizeHandler.x); } resizeHandler.anchors.right = resizeHandler.parent.right; if (root.resizeGuide !== null) { root.resizeGuide.destroy(); } } } } Component { id: resizeMarker Popup { y: resizeHandler.y + parent.height - 6 height: table.height + 6 width: 2 background: Rectangle { color: sysPalette.window } contentItem: Item {} } } } // Table header cell end } } Item { Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: 100 ScrollView { id: tableScrollView anchors.fill: parent ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AlwaysOn TableView { id: table objectName: "rdm_value_tab_table" focus: true clip: true width: parent.width onWidthChanged: forceLayout() columnSpacing: 1 rowSpacing: 1 reuseItems: false model: searchModel ? searchModel : null // Proxy model row index from 0 to maxItemsOnPage property int currentRow: -1 property var searchField property var currentStart: 0 property int maxItemsOnPage: keyTab.keyModel ? keyTab.keyModel.pageSize : 100 property int currentPage: currentStart / maxItemsOnPage + 1 property int totalPages: keyTab.keyModel ? Math.ceil(keyTab.keyModel.totalRowCount / maxItemsOnPage) : 0 property bool forceLoading: false property int firstColumnWidth: 75 property int valueColumnWidth: keyTab.keyModel && keyTab.keyModel.columnNames.length == 2? root.width - 200 - table.firstColumnWidth - table.columnSpacing : (root.width - 200 - table.firstColumnWidth - table.columnSpacing) / 2 property var valueColumnWidthOverrides: QtObject {} Keys.onUpPressed: { if (currentRow > 0) { currentRow--; } else { currentRow = 0; } } Keys.onDownPressed: { if (currentRow < rows - 1) { currentRow++; } else { currentRow = rows - 1; } } columnWidthProvider: function (column) { if (column === 0) { return firstColumnWidth } if (valueColumnWidthOverrides && valueColumnWidthOverrides[column] !== undefined) { return valueColumnWidthOverrides[column] } return valueColumnWidth } property int minColWidth: 50 function setColumnWidth(index, width) { if (width < minColWidth) width = minColWidth if (keyTab.keyModel.columnNames.length == 2) { table.valueColumnWidthOverrides[index] = width; } else { for (var i=1; i < 3; i++) { if (i === index) { table.valueColumnWidthOverrides[i] = width; } else { table.valueColumnWidthOverrides[i] = root.width - 200 - table.firstColumnWidth - table.columnSpacing - width; if (table.valueColumnWidthOverrides[i] < minColWidth) return setColumnWidth(i, minColWidth) } } } table.forceLayout() tableHeader.model = [] tableHeader.model = keyTab.keyModel.columnNames } Connections { target: root function onWidthChanged() { table.valueColumnWidthOverrides = {}; } } Component.onCompleted: keyTab.table = table delegate: DelegateChooser { DelegateChoice { column: 0 // NOTE: rowNumber - key model zero based index from 0 to rowsCount // NOTE: row - from 0 to pageSize ValueTableCell { objectName: "rdm_value_table_cell_col1" implicitWidth: table.firstColumnWidth implicitHeight: 30 text: Number(rowNumber) + 1 selected: table.currentRow === row onClicked: { table.currentRow = row table.forceActiveFocus() } } } DelegateChoice { column: 1 ValueTableCell { objectName: "rdm_value_table_cell_col2" implicitWidth: table.valueColumnWidth implicitHeight: 30 text: renderText(display) selected: table.currentRow === row onClicked: { table.currentRow = row table.forceActiveFocus() } } } DelegateChoice { column: 2 ValueTableCell { objectName: "rdm_value_table_cell_col3" implicitWidth: table.valueColumnWidth implicitHeight: 30 selected: table.currentRow === row onClicked: { table.currentRow = row table.forceActiveFocus() } text: { if (display === "" || !isMultiRow) { return "" } if (keyType == "zset") { return Number(display) } return renderText(display) } } } } OkDialogOverlay { id: valueErrorNotification visible: false } Connections { id: keyModelConnections ignoreUnknownSignals: true target: keyTab.keyModel ? keyTab.keyModel : null function onError(error) { valueErrorNotification.text = error valueErrorNotification.open() } function onIsLoadedChanged() { console.log("model loaded (qml)") if (keyTab.keyModel.totalRowCount === 0) { console.log("Load rows count") keyTab.keyModel.loadRowsCount() } else { console.log("Load rows") keyTab.keyModel.loadRows(currentStart, maxItemsOnPage) } } function onTotalRowCountChanged() { keyTab.keyModel.loadRows(table.currentStart, table.maxItemsOnPage) } function onRowsLoaded() { console.log("rows loaded") wrapper.hideLoader() keyTab.searchModel = keyTab.searchModelComponent.createObject(keyTab) if (isMultiRow) { valueEditor.clear() } else { valueEditor.loadRowValue(0) } table.forceLayout() } } function goToPage(page) { var firstItemOnPage = table.maxItemsOnPage * (page - 1) if (table.currentStart === firstItemOnPage) return table.currentStart = firstItemOnPage resetCurrentRow() loadValue() } function goToPrevPage() { console.log('goto prev page') if (table.currentPage - 1 < 1) return goToPage(table.currentPage - 1) } function goToNextPage() { console.log('goto next page') if (table.totalPages < table.currentPage + 1) return goToPage(table.currentPage + 1) } function loadValue() { console.log("Load value") if (!keyTab.keyModel) { console.log("Model is not ready", keyViewModel) return } wrapper.showLoader() if (isMultiRow && keyTab.keyModel.totalRowCount === 0) { console.log("Load rows count") keyTab.keyModel.loadRowsCount() } else { console.log("Load rows") keyTab.keyModel.loadRows(currentStart, maxItemsOnPage) } } function resetCurrentRow() { table.currentRow = -1 } function sort(role, order) { table.model.setSortRole(role) table.model.setSortOrder(order) } onRowsChanged: wrapper.hideLoader() onCurrentRowChanged: { console.log("Current row in table changed: ", currentRow) if (currentRow >= 0) { valueEditor.loadRowValue(currentStart + table.model.getOriginalRowIndex(currentRow)) } } } } } Loader { id: filtersLoader Layout.fillWidth: true Layout.preferredHeight: 40 visible: status === Loader.Ready source: keyModel && (keyType === "list" || keyType === "stream") ? "./filters/" + String(keyType)[0].toUpperCase() + String(keyType).substring(1) +"Filters.qml" : "" } } ValueTableActions {} } } ================================================ FILE: src/qml/value-editor/ValueTableActions.qml ================================================ import QtQuick 2.13 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.13 import QtQuick.Controls.Styles 1.1 import QtQuick.Window 2.2 import "./editors/editor.js" as Editor import "./../common/platformutils.js" as PlatformUtils import "./../common" import rdm.models 1.0 import Qt.labs.qmlmodels 1.0 ColumnLayout { Layout.fillHeight: true Layout.preferredWidth: 200 Layout.maximumWidth: 200 Layout.alignment: Qt.AlignTop Layout.bottomMargin: 10 BetterButton { objectName: "rdm_value_tab_add_row_btn" Layout.fillWidth: true text: qsTranslate("RESP","Add Row") iconSource: PlatformUtils.getThemeIcon("add.svg") onClicked: { addRowDialog.open() } BetterDialog { id: addRowDialog title: keyType === "hyperloglog"? qsTranslate("RESP","Add Element to HLL") : qsTranslate("RESP","Add Row") width: 550 height: 400 contentItem: Rectangle { color: sysPalette.base implicitWidth: 800 implicitHeight: PlatformUtils.isOSX()? 680 : 600 ColumnLayout { anchors.fill: parent anchors.margins: 10 Loader { id: valueAddEditor Layout.fillWidth: true Layout.fillHeight: true property int currentRow: -1 objectName: "rdm_add_row_dialog" source: keyTab.keyModel ? Editor.getEditorByTypeString(keyType, true) : "" onLoaded: { item.state = "add" item.initEmpty() keyTab.addRowDialog = addRowDialog } } } } footer: BetterDialogButtonBox { BetterButton { DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole objectName: "rdb_add_row_dialog_add_button" text: qsTranslate("RESP","Add") onClicked: { if (!valueAddEditor.item) return false valueAddEditor.item.getValue(true, function (valid, row){ if (!valid) { return; } keyTab.keyModel.addRow(row) keyTab.keyModel.reload() valueAddEditor.item.reset() valueAddEditor.item.initEmpty() addRowDialog.close() }); } } BetterButton { text: qsTranslate("RESP", "Cancel") DialogButtonBox.buttonRole: DialogButtonBox.RejectRole } } visible: false } } BetterButton { objectName: "rdm_value_editor_delete_row_btn" Layout.fillWidth: true text: qsTranslate("RESP","Delete row") iconSource: PlatformUtils.getThemeIcon("delete.svg") enabled: table.currentRow != -1 onClicked: { if (keyTab.keyModel.totalRowCount === 1) { deleteRowConfirmation.text = qsTranslate("RESP","The row is the last one in the key. After removing it key will be deleted.") } else { deleteRowConfirmation.text = qsTranslate("RESP","Do you really want to remove this row?") } var rowIndex = table.currentStart + table.model.getOriginalRowIndex(table.currentRow) console.log("removing row", rowIndex) deleteRowConfirmation.rowToDelete = rowIndex deleteRowConfirmation.open() } BetterMessageDialog { id: deleteRowConfirmation title: qsTranslate("RESP","Delete row") text: "" onYesClicked: { console.log("remove row in key") keyTab.keyModel.deleteRow(rowToDelete) table.resetCurrentRow() valueEditor.clear() table.model.invalidate() } visible: false property int rowToDelete } } BetterButton { objectName: "rdm_value_editor_reload_value_btn" Layout.fillWidth: true text: qsTranslate("RESP","Reload Value") iconSource: PlatformUtils.getThemeIcon("refresh.svg") action: reLoadAction Action { id: reLoadAction shortcut: StandardKey.Refresh onTriggered: { reloadValue() } } } RowLayout { Layout.fillWidth: true BetterTextField { id: searchField Layout.fillWidth: true readOnly: keyTab.keyModel ? keyTab.keyModel.singlePageMode : false placeholderText: qsTranslate("RESP","Search on page...") Component.onCompleted: { table.searchField = searchField } } BetterButton { id: clearGlobalSearch visible: keyTab.keyModel ? keyTab.keyModel.singlePageMode : false iconSource: PlatformUtils.getThemeIcon("clear.svg") onClicked: { wrapper.showLoader() searchField.text = "" keyTab.keyModel.singlePageMode = false reLoadAction.trigger() } } } BetterButton { id: globalSearch Layout.fillWidth: true iconSource: PlatformUtils.getThemeIcon("loader.svg") text: qsTranslate("RESP","Full Search") onClicked: { wrapper.showLoader() keyTab.keyModel.singlePageMode = true keyTab.keyModel.loadRows(0, keyTab.keyModel.totalRowCount) } } Item { Layout.fillWidth: true Layout.fillHeight: true } Pagination { id: pagination Layout.fillWidth: true visible: keyTab.keyModel ? isMultiRow : false } } ================================================ FILE: src/qml/value-editor/ValueTableCell.qml ================================================ import QtQuick 2.0 import QtQuick.Controls 2.0 Item { id: root property alias text: textItem.text property alias color: background.color property bool selected: false signal clicked Rectangle { id: background anchors.fill: parent border.color: root.selected ? sysPalette.midlight : sysPalette.mid border.width: 1 color: root.selected ? sysPalette.highlight : sysPalette.base clip: true TextInput { id: textItem anchors.centerIn: parent wrapMode: Text.WrapAnywhere color: root.selected ? sysPalette.highlightedText : sysPalette.text readOnly: true selectByMouse: true autoScroll: false } } function renderText(t) { if (t === "") return t if (qmlUtils.binaryStringLength(t) > 100) { return qmlUtils.printable(t, false, 100) + "..." } return qmlUtils.printable(t) + (textItem.lineCount > 1 ? '...' : '') } MouseArea { anchors.fill: parent enabled: !root.selected onClicked: { root.clicked() } } } ================================================ FILE: src/qml/value-editor/ValueTabs.qml ================================================ import QtQuick 2.13 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.13 import QtQuick.Controls.Styles 1.1 import QtQuick.Window 2.2 import "./editors/editor.js" as Editor import "./../common/platformutils.js" as PlatformUtils import "./../common" import rdm.models 1.0 import Qt.labs.qmlmodels 1.0 Repeater { BetterTab { id: keyTab objectName: "rdm_value_tab" Component { id: valueTabButton BetterTabButton { objectName: "rdm_value_tab_btn" icon.source: PlatformUtils.getThemeIcon("key.svg") text: tabName tooltip: keyModel && tabName <= keyName? keyName : "" onCloseClicked: { if (valueEditor.item && valueEditor.item.isEdited() && keyType != "stream") { closeConfirmation.open() } else { valuesModel.closeTab(keyIndex) } } BetterMessageDialog { id: closeConfirmation title: qsTranslate("RESP","Changes are not saved") text: qsTranslate("RESP","Do you want to close key tab without saving changes?") visible: false onYesClicked: { valuesModel.closeTab(keyIndex) } } } } property int tabIndex: keyIndex property var table property var valueEditor property var searchModel property var tabButton property bool loadingModel: showLoader property variant keyModel: keyViewModel property var addRowDialog onKeyModelChanged: { console.log("keyModel changed") if (keyModel && keyModel.isLoaded) { table.forceLoading = false table.currentStart = 0 table.searchField.text = "" if (valueEditor.item) valueEditor.item.reset() table.loadValue() } } property Component searchModelComponent: Component { SortFilterProxyModel { source: keyViewModel sortOrder: Qt.AscendingOrder sortCaseSensitivity: Qt.CaseInsensitive sortRole: keyTab.keyModel && keyTab.keyModel.isLoaded ? "row" : "" filterString: table.searchField.text filterSyntax: SortFilterProxyModel.Wildcard filterCaseSensitivity: Qt.CaseInsensitive filterKeyColumn: -1 onFilterStringChanged: { table.resetCurrentRow() } Component.onCompleted: { if (keyTab.keyModel && keyTab.keyModel.isLoaded && keyTab.keyModel.singlePageMode) { // NOTE(u_glide): disable live search in all values filterString = table.searchField.text } } } } Keys.onPressed: { var reloadKey = event.key == Qt.Key_F5 || (event.key == Qt.Key_R && (event.modifiers & Qt.ControlModifier)) || (event.key == Qt.Key_R && (event.modifiers & Qt.MetaModifier)) if (reloadKey && keyModel.isLoaded) { console.log("Reload") keyModel.reload() } } Component.onCompleted: { keyTab.focus = true keyTab.forceActiveFocus() // Update tabBar if (!tabButton) { tabButton = valueTabButton.createObject(keyTab); tabButton.self = tabButton; tabButton.tabRef = keyTab; tabBar.addItem(tabButton) tabBar.activateTabButton(tabButton) tabs.activateTab(keyTab) } } onActivate: { valuesModel.setCurrentTab(keyIndex) } function reloadValue() { console.log("Reload value in tab") keyTab.keyModel.reload() if (isMultiRow) { valueEditor.clear() table.resetCurrentRow() if (table.currentPage > table.totalPages) { table.goToPage(1) } } } Rectangle { id: wrapper color: sysPalette.base anchors.fill: parent anchors.margins: 5 function showLoader() { uiBlocker.visible = true } function hideLoader() { uiBlocker.visible = false } ColumnLayout { visible: !loadingModel anchors.fill: parent spacing: 5 RowLayout { Layout.preferredHeight: 30 Layout.minimumHeight: 30 Layout.fillWidth: true spacing: 5 BetterLabel { Layout.preferredWidth: isMultiRow ? 70 : 90 text: keyModel? keyType.toUpperCase() + ":" : ""; font.bold: true horizontalAlignment: Text.AlignHCenter } BetterTextField { id: keyNameField Layout.fillWidth: true text: keyModel? keyName : "" readOnly: true objectName: "rdm_key_name_field" ImageButton { anchors.right: parent.right anchors.rightMargin: 5 anchors.verticalCenter: parent.verticalCenter iconSource: PlatformUtils.getThemeIcon("cleanup.svg") tooltip: qsTranslate("RESP","Rename key") objectName: "rdm_key_rename_btn" onClicked: renameConfirmation.open() BetterDialog { id: renameConfirmation title: qsTranslate("RESP","Rename key") width: 520 RowLayout { implicitWidth: 500 implicitHeight: 100 width: 500 BetterLabel { text: qsTranslate("RESP","New name:") } BetterTextField { id: newKeyName; Layout.fillWidth: true; objectName: "rdm_rename_key_field" text: keyModel? keyName : "" } } onAccepted: { if (newKeyName.text.length == 0) { return open() } keyTab.keyModel.renameKey(newKeyName.text) } visible: false } } } BetterLabel { visible: keyType === "hyperloglog"; text: qsTranslate("RESP","Size: ") + keyRowsCount } BetterButton { Layout.preferredWidth: isMultiRow? 92 : 98 text: qsTranslate("RESP","TTL:") + keyTtl objectName: "rdm_key_ttl_value" tooltip: keyTtl BetterDialog { id: setTTLConfirmation title: qsTranslate("RESP","Set key TTL") width: 520 RowLayout { implicitWidth: 500 implicitHeight: 100 width: 500 BetterLabel { text: qsTranslate("RESP","New TTL:") } BetterTextField { id: newTTL; Layout.fillWidth: true; objectName: "rdm_set_ttl_key_field" inputMethodHints: Qt.ImhDigitsOnly validator: IntValidator{bottom: 1} } } footer: BetterDialogButtonBox { BetterButton { text: qsTranslate("RESP","Save") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole } BetterButton { objectName: "rdm_persist_key_btn" text: qsTranslate("RESP","Persist key") onClicked: { keyTab.keyModel.persistKey() setTTLConfirmation.close() } } BetterButton { text: qsTranslate("RESP","Cancel") onClicked: setTTLConfirmation.close() } } onAccepted: { if (newTTL.text.length == 0) { return open() } keyTab.keyModel.setTTL(newTTL.text) } visible: false } onClicked: { if (keyTtl > 0) { newTTL.text = ""+keyTtl } else { newTTL.text = "" } setTTLConfirmation.open() } } BetterButton { objectName: "rdm_value_tab_delete_btn" Layout.preferredWidth: 98 text: qsTranslate("RESP","Delete") iconSource: PlatformUtils.getThemeIcon("delete.svg") BetterMessageDialog { id: deleteConfirmation title: qsTranslate("RESP","Delete key") text: qsTranslate("RESP","Do you really want to delete this key?") onYesClicked: { keyTab.keyModel.removeKey() } visible: false } onClicked: { deleteConfirmation.open() } } BetterButton { objectName: "rdm_value_editor_reload_value_btn" text: qsTranslate("RESP","Reload Value") onClicked: reloadValue() visible: !isMultiRow iconSource: PlatformUtils.getThemeIcon("refresh.svg") } } BetterSplitView { orientation: Qt.Vertical Layout.fillHeight: true Layout.fillWidth: true // Table ValueTable { id: navigationTable Layout.fillWidth: true Layout.fillHeight: false Layout.bottomMargin: 20 SplitView.minimumHeight: 300 visible: keyModel? isMultiRow : false } // Value editor Item { id: editorWrapper SplitView.fillWidth: true SplitView.fillHeight: !isMultiRow Layout.topMargin: 20 SplitView.minimumHeight: 220 BetterDialog { id: fullScreenEditorDialog title: tabName width: approot.width * 0.9 height: approot.height * 0.9 footer: null Rectangle { id: fullscreenEditorParent anchors.fill: parent implicitHeight: 500 implicitWidth: 800 } onClosed: { editor.state = "default" } } Rectangle { id: editor anchors.fill: parent color: sysPalette.base state: "default" states: [ State { name: "full_screen" ParentChange { target: editor; parent: fullscreenEditorParent;} PropertyChanges { target: fullScreenEditorDialog visible: true } PropertyChanges { target: fullScreenModeBtn visible: false } }, State { name: "default" ParentChange { target: editor; parent: editorWrapper; } PropertyChanges { target: fullScreenEditorDialog visible: false } PropertyChanges { target: fullScreenModeBtn visible: true } } ] ColumnLayout { id: editorLayout anchors.fill: parent anchors.margins: 5 spacing: 10 Loader { id: valueEditor objectName: "rdm_value_editor_loader" Layout.topMargin: 5 Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: 180 Component.onCompleted: { keyTab.valueEditor = valueEditor } property int currentRow: -1 source: keyTab.keyModel? Editor.getEditorByTypeString(keyType, false) : "" function loadRowValue(row) { console.log("loading row value", row) if (valueEditor.item) { var rowValue = keyTab.keyModel.getRow(row) valueEditor.currentRow = row valueEditor.item.reset() valueEditor.item.defaultFormatter = defaultFormatter valueEditor.item.setValue(rowValue) } else { console.log("cannot load row value - item is missing") } } function clear() { if (valueEditor.item) { currentRow = -1 valueEditor.item.keyType = Qt.binding(function() { return keyType }); valueEditor.item.reset() } } onLoaded: clear() } } } } // Value editor end } } Rectangle { id: uiBlocker visible: loadingModel anchors.fill: parent color: loadingModel? Qt.rgba(0, 0, 0, 0) : Qt.rgba(0, 0, 0, 0.1) Item { anchors.fill: parent ColumnLayout { anchors.centerIn: parent; BusyIndicator { Layout.alignment: Qt.AlignHCenter; running: true } BetterLabel { visible: loadingModel text: tabName } } } MouseArea { anchors.fill: parent } } } } } ================================================ FILE: src/qml/value-editor/editors/AbstractEditor.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.1 ColumnLayout { state: "edit" property string keyType: "" states: [ State { name: "new"}, // Creating new key State { name: "add"}, // Adding new value to existing key State { name: "edit"} // Editing existing key ] function initEmpty() { console.exception("Not implemented") } function getValue(validateVal, callback) { console.exception("Not implemented") } function isEdited() { console.exception("Not implemented") } function setValue(value) { console.exception("Not implemented") } function reset() { console.exception("Not implemented") } } ================================================ FILE: src/qml/value-editor/editors/HashItemEditor.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.1 import "." AbstractEditor { id: root anchors.fill: parent property bool active: false property alias defaultFormatter: textArea.defaultFormatter MultilineEditor { id: keyText fieldLabel: qsTranslate("RESP","Key:") Layout.fillWidth: true Layout.minimumHeight: 30 Layout.preferredHeight: root.state == "new"? 70: 140 value: "" enabled: root.active || root.state !== "edit" showToolBar: root.state == "edit" showSaveBtn: root.state == "edit" showFormatters: root.state == "edit" objectName: "rdm_key_hash_key_field" formatterSettingsPrefix: "hash_key_" } MultilineEditor { id: textArea Layout.fillWidth: true Layout.fillHeight: true enabled: root.active || root.state !== "edit" showToolBar: root.state == "edit" showFormatters: root.state == "edit" objectName: "rdm_key_hash_text_field" function validationRule(raw) { return true; } } function initEmpty() { keyText.initEmpty() textArea.initEmpty() } function getValue(validateVal, callback) { keyText.validate(function (keyTextValid, rawKey) { if (!validateVal) { return callback(keyTextValid, {"value": "", "key": rawKey}); } else { textArea.validate(function (textAreaValid, rawValue) { return callback(keyTextValid && textAreaValid, {"value": rawValue, "key": rawKey}); }); } }); } function setValue(rowValue) { if (!rowValue) return active = true keyText.loadFormattedValue(rowValue['key']) textArea.loadFormattedValue(rowValue['value']) } function isEdited() { return textArea.isEdited || keyText.isEdited } function reset() { textArea.reset() keyText.reset() active = false } } ================================================ FILE: src/qml/value-editor/editors/MultilineEditor.qml ================================================ import QtQuick 2.5 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import Qt.labs.settings 1.0 import rdm.models 1.0 import "../../common/" import "../../common/platformutils.js" as PlatformUtils Item { id: root property bool enabled property string textColor property int imgBtnWidth: PlatformUtils.isOSXRetina(Screen)? 18 : 22 property int imgBtnHeight: PlatformUtils.isOSXRetina(Screen)? 18 : 22 property bool showToolBar: false property bool showSaveBtn: false property bool showFormatters: true property bool showOnlyRWformatters: false property bool showValueSize: true property string fieldLabel: qsTranslate("RESP","Value") + ":" property bool isEdited: false property var value property int valueCompression: 0 property alias readOnly: textView.readOnly property string formatterSettingsPrefix: "" property string lastSelectedFormatterSetting: "last_selected_" + root.formatterSettingsPrefix + "formatter" property string lastSelectedManualDecompression: "last_selected_" + root.formatterSettingsPrefix + "decompression" property string defaultFormatter: "auto" property var __formatterCombobox: formatterSelector property var __textView: textView function __getFormattingContext() { return { "redis-key-name": root.parent.state === "new"? newKeyName.value : keyName, "redis-key-type": keyType, } } function initEmpty() { // init editor with empty model textView.model = qmlUtils.wrapLargeText("") textView.readOnly = false textView.textFormat = TextEdit.PlainText } function validationRule(raw) { return qmlUtils.binaryStringLength(raw) > 0 } function validate(callback) { loadRawValue(function (error, raw) { if (error) { notification.showError(error) return callback(false, raw); } var valid = validationRule(raw) if (valid) { hideValidationError() } else { showValidationError(qsTranslate("RESP", "Enter valid value")) } return callback(valid, raw) }); } function compress(val) { if (valueCompression > 0) { return qmlUtils.compress(val, valueCompression) } else { return val } } function loadRawValue(callback) { function process(formattedValue) { var formatter = valueFormattersModel.get(formatterSelector.currentIndex) formatter.getRaw(formattedValue, function (error, raw) { var compressed = compress(raw); return callback(error, compressed) }, __getFormattingContext()) } if (textView.format === "json") { formatterSelector.model.getJSONFormatter().getRaw(textView.model.getText(), function (jsonError, plainText) { if (jsonError) { return callback(jsonError, "") } process(plainText) }, __getFormattingContext()) } else { process(textView.model.getText()) } } function loadFormattedValue(val) { var guessFormatter = false; if (val) { root.value = val; guessFormatter = true; } var isBin = qmlUtils.isBinaryString(root.value) binaryFlag.visible = isBin if (isBin && qmlUtils.binaryStringLength(root.value) > appSettings.valueSizeLimit) { largeValueDialog.visible = true; root.showFormatters = false; textView.model = qmlUtils.wrapLargeText(qmlUtils.printable(root.value, false, 50000)); textView.readOnly = true; saveBtn.enabled = false; return; } else { largeValueDialog.visible = false root.showFormatters = true } var continueFormatting = function (guessFormatter) { if (!root.value) { console.log("Empty value. Skipping formatting stage"); return; } // NOTE(u_glide): -1 means "not set", 0 - unknown or not compressed if (valueCompression < 0) { var compressionMethod = qmlUtils.isCompressed(root.value); if (compressionMethod > 0) { valueCompression = compressionMethod root.value = qmlUtils.decompress(root.value, valueCompression) isBin = qmlUtils.isBinaryString(root.value) var compression = qmlUtils.compressionAlgName(valueCompression); // NOTE(u_glide): hint PHP formatter if MAGENTO/PHP compression detected if (guessFormatter && compression && compression.startsWith("magento-session-")) { formatterSelector._select("php"); guessFormatter = false; } } // NOTE(u_glide): try to decompress using last "no magic" compression if (isBin && valueCompression < 0 && qmlUtils.binaryStringLength(root.value) <= appSettings.valueSizeLimit) { noMagicCompressionSelector.loadLastUsed() } } // If current formatter is plain text - try to guess formatter if (guessFormatter) { _guessFormatter(root.value, isBin, function() { _loadFormatter(isBin) }) } else { _loadFormatter(isBin) } }; if (guessFormatter) { console.log("Default formatter:", root.defaultFormatter) var formatterOverride = defaultFormatterSettings.value( root.formatterSettingsPrefix + keyName, "" ); if (!formatterOverride) { if (root.defaultFormatter == "last_used") { formatterOverride = defaultFormatterSettings.value(root.lastSelectedFormatterSetting, ""); } else if (root.defaultFormatter != "auto") { formatterOverride = root.defaultFormatter; } if (!formatterOverride || root.defaultFormatter == "auto") { return continueFormatting(true) } } var expectedFormatter = formatterSelector.find(formatterOverride); if (expectedFormatter === -1) { console.log("Formatter", formatterOverride, " is not loaded. Fallback to guessing...") return continueFormatting(true) } console.log("Formatter override:", formatterOverride) var cFormatter = formatterSelector.model.get(expectedFormatter) return cFormatter.isValid(root.value, function (isValid) { var compressionMethod = qmlUtils.isCompressed(root.value); if (isValid || compressionMethod > 0) { formatterSelector._select(formatterOverride) continueFormatting(false) } else { console.log("Formatter", formatterOverride, " cannot decode value. Fallback to guessing...") continueFormatting(true) } }, __getFormattingContext()) } else { continueFormatting(false) } } function hintFormatter(name) { if (showOnlyRWformatters) { rwFormatterSelector._select(name) formatterSelector._select(name) } else { formatterSelector._select(name) } _loadFormatter(false) } function _guessFormatter(value, isBin, callback) { console.log("Guessing formatter") var candidates = valueFormattersModel.guessFormatter(value, isBin) console.log("candidates:", candidates) if (Array.isArray(candidates)) { for (var index in candidates) { var cFormatter = formatterSelector.model[candidates[index]] cFormatter.isValid(root.value, function (isValid) { if (isValid) { formatterSelector.currentIndex = candidates[index] callback() } }, __getFormattingContext()) if (formatterSelector.currentIndex !== 0) break } } else { formatterSelector.currentIndex = candidates callback() } } function _loadFormatter(isBin) { if (!(0 <= formatterSelector.currentIndex && formatterSelector.currentIndex < formatterSelector.count)) { formatterSelector.currentIndex = formatterSelector.model.getDefaultFormatter(isBin) } var formatter = formatterSelector.model.get(formatterSelector.currentIndex) uiBlocker.visible = true function processFormattingResult(error, formatted, isReadOnly, format) { textView.textFormat = (format === "html") ? TextEdit.RichText : TextEdit.PlainText; console.log("format", format) if (error || (!formatted && root.value)) { if (formatted) { textView.model = qmlUtils.wrapLargeText(formatted) } else if (!error) { formatterSelector.currentIndex = valueFormattersModel.guessFormatter(root.value, isBin) return _loadFormatter(isBin) } textView.readOnly = isReadOnly textView.format = "text" root.isEdited = false uiBlocker.visible = false var details if (error.length > 200) { details = error error = qsTranslate("RESP","Formatting error") } else { details = "" } notification.showError(error || qsTranslate("RESP","Unknown formatter error (Empty response)"), details) return } if (format === "image") { imageView.source = formatted; } else { textView.model = qmlUtils.wrapLargeText(formatted) } textView.readOnly = isReadOnly textView.format = format root.isEdited = false uiBlocker.visible = false } formatter.getFormatted(root.value, function (error, formatted, isReadOnly, format) { textView.format = format if (format === "json" && formatter["name"] !== "JSON" && !error) { formatterSelector.model.getJSONFormatter().getFormatted(formatted, function (jsonError, plainText) { if (jsonError) { processFormattingResult(jsonError, formatted, isReadOnly, format) } else { processFormattingResult(jsonError, plainText, isReadOnly, format) } }, __getFormattingContext()) } else { processFormattingResult(error, formatted, isReadOnly, format) } }, __getFormattingContext()) } function reset() { if (textView.model) textView.model.cleanUp() if (textView.model) { qmlUtils.deleteTextWrapper(textView.model) } textView.model = null root.value = "" root.isEdited = false root.valueCompression = -1 binaryFlag.visible = false saveBtnTimer.resetSaveBtn() hideValidationError() } function showValidationError(msg) { validationError.text = msg validationError.visible = true } function hideValidationError() { validationError.visible = false } ColumnLayout { anchors.fill: parent RowLayout { Layout.fillWidth: true spacing: 5 BetterLabel { text: root.fieldLabel } TextEdit { text: qsTranslate("RESP", "Size: ") + qmlUtils.humanSize(qmlUtils.binaryStringLength(value)); readOnly: true; selectByMouse: true color: "#ccc" visible: showValueSize } BetterLabel { id: binaryFlag; text: qsTranslate("RESP","[Binary]"); visible: false; color: "green"; } Item { Layout.fillWidth: true } BetterLabel { visible: showFormatters; text: qsTranslate("RESP","View as:") } BetterComboBox { id: formatterSelector visible: showFormatters && !showOnlyRWformatters width: 200 model: valueFormattersModel textRole: "name" objectName: "rdm_value_editor_formatter_combobox" onActivated: { currentIndex = index console.log("Set default formatter '" + currentText + "' for key " + keyName) defaultFormatterSettings.setValue(root.formatterSettingsPrefix + keyName, currentText) defaultFormatterSettings.setValue(root.lastSelectedFormatterSetting, currentText) loadFormattedValue() } } BetterComboBox { id: rwFormatterSelector visible: showFormatters && showOnlyRWformatters width: 200 model: valueFormattersModel.rwFormatters textRole: "name" objectName: "rdm_value_editor_rw_formatter_combobox" onActivated: { formatterSelector.currentIndex = valueFormattersModel.getFormatterIndex(currentText); } } BetterLabel { visible: noMagicCompressionSelector.visible text: noMagicCompressionSelector.enabled? qsTranslate("RESP","Try to decompress:") : qsTranslate("RESP","Decompressed:") } BetterComboBox { id: noMagicCompressionSelector Layout.preferredWidth: 120 Layout.fillWidth: true objectName: "rdm_value_editor_compression_combobox" textRole: "text" visible: { console.log("keyType:", keyType) return binaryFlag.visible && keyType != "hyperloglog" && qmlUtils.binaryStringLength(root.value) <= appSettings.valueSizeLimit || root.valueCompression > 0 } onEnabledChanged: { indicator.visible = enabled; } flat: !enabled enabled: { return binaryFlag.visible && root.valueCompression < 1 || root.valueCompression >= firstNoMagicMethod; } displayText: { if (0 < root.valueCompression && root.valueCompression < noMagicCompressionSelector.firstNoMagicMethod) { return qmlUtils.compressionAlgName(root.valueCompression) } else { return currentText; } } property int firstNoMagicMethod: { var noMagicCompress = qmlUtils.compressionMethodsNoMagic(); return noMagicCompress[noMagicCompress.length - 1]; } model: { var noMagicCompress = qmlUtils.compressionMethodsNoMagic(); var modelList = []; for (var index in noMagicCompress) { var label = qmlUtils.compressionAlgName(noMagicCompress[index]); if (label === "unknown") { label = ""; } modelList.push({"value": noMagicCompress[index], "text": label}); } return modelList; } function loadLastUsed() { var lastSelected = defaultCompressionSettings.value( root.formatterSettingsPrefix + keyName, defaultCompressionSettings.value(root.lastSelectedManualDecompression, "") ); selectItem(lastSelected); } onActivated: { console.log("Try to decompress as", currentText) var expectedCompression = model[currentIndex]['value']; if (expectedCompression == 0) { valueCompression = 0 defaultCompressionSettings.setValue(root.formatterSettingsPrefix + keyName, "") defaultCompressionSettings.setValue(root.lastSelectedManualDecompression, "") root.loadFormattedValue() return } var decompressed = qmlUtils.decompress(root.value, expectedCompression) if (qmlUtils.binaryStringLength(decompressed) > 0) { binaryFlag.visible = qmlUtils.isBinaryString(root.value) valueCompression = expectedCompression; root.loadFormattedValue(decompressed) defaultCompressionSettings.setValue(root.formatterSettingsPrefix + keyName, currentText) defaultCompressionSettings.setValue(root.lastSelectedManualDecompression, currentText) noMagicCompressionSelector.enabled = false; } else { notification.showError(qsTranslate("RESP","Cannot decompress value using ") + currentText) defaultCompressionSettings.setValue(root.formatterSettingsPrefix + keyName, "") defaultCompressionSettings.setValue(root.lastSelectedManualDecompression, "") valueCompression = 0 currentIndex = 0; } } } BetterLabel { visible: !showFormatters && qmlUtils.binaryStringLength(root.value) > appSettings.valueSizeLimit text: qsTranslate("RESP","Large value (>150kB). Formatters are not available.") color: "red" } BetterButton { iconSource: PlatformUtils.getThemeIcon("add.svg") Layout.alignment: Qt.AlignHCenter text: qsTranslate("RESP","Add Element"); visible: (keyType === "hyperloglog" || keyType === "bf" || keyType === "cf") onClicked: { keyTab.addRowDialog.open() } } RowLayout { id: valueEditorToolBar Layout.preferredWidth: isMultiRow ? 200 : 208 Layout.maximumWidth: isMultiRow ? 200 : 208 visible: showToolBar RowLayout { Layout.fillWidth: true Layout.preferredWidth: 98 ImageButton { id: copyValueToClipboardBtn iconSource: PlatformUtils.getThemeIcon("copy_2.svg") implicitWidth: imgBtnWidth implicitHeight: imgBtnHeight imgWidth: imgBtnWidth imgHeight: imgBtnHeight Layout.alignment: Qt.AlignHCenter tooltip: qsTranslate("RESP","Copy to Clipboard") enabled: root.value !== "" onClicked: copyValue() function copyValue() { if (value) { qmlUtils.copyToClipboard(textView.model.getText()) } } } SaveToFileButton { id: saveAsBtn objectName: "rdm_save_value_to_file_btn" Layout.alignment: Qt.AlignHCenter implicitWidth: imgBtnWidth implicitHeight: imgBtnHeight imgWidth: imgBtnWidth imgHeight: imgBtnHeight enabled: root.value !== "" && root.showFormatters shortcutText: qmlUtils.standardKeyToString(StandardKey.SaveAs) } SaveToFileButton { id: saveAsRawBtn objectName: "rdm_save_raw_value_to_file_btn" raw: true Layout.alignment: Qt.AlignHCenter implicitWidth: imgBtnWidth implicitHeight: imgBtnHeight imgWidth: imgBtnWidth imgHeight: imgBtnHeight enabled: root.value !== "" } } ImageButton { id: fullScreenModeBtn iconSource: editor.state === "default"? PlatformUtils.getThemeIcon("maximize.svg") : PlatformUtils.getThemeIcon("minimize.svg") implicitWidth: imgBtnWidth implicitHeight: imgBtnHeight imgWidth: imgBtnWidth * 0.8 imgHeight: imgBtnHeight * 0.8 tooltip: (editor.state === "default"? "" : qsTranslate("RESP","Exit ")) + qsTranslate("RESP","Full Screen Mode") onClicked: { editor.state = editor.state === "default"? "full_screen" : "default" editor.forceActiveFocus() } } BetterButton { id: saveBtn objectName: "rdm_value_editor_save_btn" state: "default" implicitWidth: isMultiRow ? 100 : 105 text: qsTranslate("RESP","Save") tooltip: qsTranslate("RESP","Save Changes") + " (" + shortcutText + ")" visible: showSaveBtn property string shortcutText: qmlUtils.standardKeyToString(StandardKey.Save) onClicked: saveChanges() function saveChanges() { if (!valueEditor.item || !valueEditor.item.isEdited()) { return } valueEditor.item.getValue(true, function (valid, row) { if (!valid) return; saveBtnTimer.start() keyTab.keyModel.updateRow(valueEditor.currentRow, row) }) } states: [ State { name: "default" PropertyChanges { target: saveBtn iconSource: PlatformUtils.getThemeIcon("save.svg") enabled: !showOnlyRWformatters && root.value !== "" && valueEditor.item.isEdited() && keyType != "stream" } }, State { name: "saving" PropertyChanges { target: saveBtn iconSource: PlatformUtils.getThemeIcon("wait.svg") enabled: false } } ] Connections { target: keyTab.keyModel ? keyTab.keyModel : null function onValueUpdated() { root.isEdited = false saveBtnTimer.resetSaveBtn() } function onError() { saveBtnTimer.resetSaveBtn() } } Timer { id: saveBtnTimer interval: 500 repeat: true triggeredOnStart: true onTriggered: saveBtn.state = "saving" function resetSaveBtn() { saveBtnTimer.stop() saveBtn.state = "default" } } } Item { width: 100 visible: !showSaveBtn && keyType == "hash" } } } Rectangle { id: searchToolbar property int lastSearchResultPosition: -1 color: sysPalette.base border.color: sysPalette.mid border.width: 1 Layout.fillWidth: true Layout.preferredHeight: 50 visible: false RowLayout { anchors.fill: parent anchors.rightMargin: 10 anchors.leftMargin: 10 Image { source: PlatformUtils.getThemeIcon("search.svg") Layout.preferredWidth: 20 Layout.preferredHeight: 20 } BetterTextField { id: searchField objectName: "rdm_value_editor_search_field" placeholderText: qsTranslate("RESP", "Search string") onTextChanged: { searchToolbar.lastSearchResultPosition = -1; noResults.visible = false; } onAccepted: submitSearchButton.performSearch() Layout.preferredWidth: 300 } BetterButton { id: submitSearchButton objectName: "rdm_value_editor_search_btn" text: searchToolbar.lastSearchResultPosition>=0 ? qsTranslate("RESP","Find Next") : qsTranslate("RESP","Find") onClicked: { performSearch() } function performSearch() { noResults.visible = false; var result = textView.model.searchText(searchField.text, searchToolbar.lastSearchResultPosition, searchRegexInText.checked) if (result[0] >= 0) { textView.currentIndex = result[0]; searchToolbar.lastSearchResultPosition = result[1] + result[3]; textView.currentItem.selectSearchResult(result[2], result[3], searchField.text); } else { noResults.text = searchToolbar.lastSearchResultPosition>=0 ? qsTranslate("RESP","Cannot find more results") : qsTranslate("RESP","Cannot find any results"); if (searchToolbar.lastSearchResultPosition>=0) { searchToolbar.lastSearchResultPosition = -1; } noResults.visible = true; } } } BetterCheckbox { id: searchRegexInText objectName: "rdm_value_editor_search_regex_checkbox" text: qsTranslate("RESP","Regex") onCheckedChanged: { searchToolbar.lastSearchResultPosition = -1; noResults.visible = false; } } BetterLabel { id: noResults objectName: "rdm_value_editor_search_no_results" visible: false } Item { Layout.fillWidth: true } ImageButton { objectName: "rdm_value_editor_search_clear_btn" Layout.preferredWidth: 20 Layout.preferredHeight: 20 imgSource: PlatformUtils.getThemeIcon("clear.svg") onClicked: { searchToolbar.visible = false; searchToolbar.lastSearchResultPosition = -1; noResults.visible = false; // TODO: clear results & selections } } } } Rectangle { id: texteditorWrapper Layout.fillWidth: true Layout.fillHeight: true Layout.preferredHeight: 100 color: sysPalette.base border.color: sysPalette.mid border.width: 1 clip: true ScrollView { id: valueScrollView anchors.fill: parent anchors.margins: 5 visible: textView.format !== "image" ScrollBar.vertical.policy: ScrollBar.AlwaysOn ScrollBar.vertical.minimumSize: 0.05 enabled: !(qmlUtils.isBinaryString(root.value) && qmlUtils.binaryStringLength(root.value) > appSettings.valueSizeLimit) ListView { id: textView anchors.fill: parent cacheBuffer: 4 highlightMoveDuration: 0 Keys.onPressed: { if (event.matches(StandardKey.Find)) { searchField.forceActiveFocus() searchToolbar.visible = true; } else if (event.matches(StandardKey.SaveAs)) { saveAsBtn.saveToFile() } else if (event.matches(StandardKey.Save)) { saveBtn.saveChanges() } } property int textFormat: TextEdit.PlainText property bool readOnly: false property string format delegate: Rectangle { color: "transparent" width: texteditorWrapper.width height: textAreaPart.contentHeight < texteditorWrapper.height? texteditorWrapper.height - 5 : textAreaPart.contentHeight function selectSearchResult(from, len, txt) { textAreaPart.select(from, from + len) textView.contentY = textView.currentItem.y + textAreaPart.cursorRectangle.y } NewTextArea { anchors.fill: parent id: textAreaPart objectName: "rdm_key_multiline_text_field_" + index enabled: root.enabled text: qmlUtils.isBinaryString(root.value) && qmlUtils.binaryStringLength(root.value) > appSettings.valueSizeLimit ? qmlUtils.printable(value, false, 50000) : value; // Show first 50KB to fit chunkSize textFormat: textView.textFormat readOnly: textView.readOnly highlightJSON: textView.format === "json" onTextChanged: { root.isEdited = true textView.model && textView.model.setTextChunk(index, textAreaPart.text) } Keys.forwardTo: [textView] } } } } Image { id: imageView anchors.fill: parent fillMode: Image.PreserveAspectFit visible: textView.format === "image" } } BetterLabel { id: validationError color: "red" visible: false } } BetterDialog { id: largeValueDialog height: 150 title: qsTranslate("RESP","Binary value is too large to display") visible: false footer: BetterDialogButtonBox { BetterButton { text: qsTranslate("RESP","OK") onClicked: largeValueDialog.close() } } RowLayout { anchors.fill: parent Text { color: sysPalette.text text: qsTranslate("RESP","Save value to file")+ ": " } SaveToFileButton { objectName: "rdm_save_large_raw_value_to_file_dialog_btn" raw: true } } Rectangle { id: uiBlocker visible: false anchors.fill: parent color: Qt.rgba(0, 0, 0, 0.1) Item { anchors.fill: parent BusyIndicator { anchors.centerIn: parent; running: true } } MouseArea { anchors.fill: parent } } } } ================================================ FILE: src/qml/value-editor/editors/ReadOnlySingleItemEditor.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.1 import "." AbstractEditor { id: root anchors.fill: parent property bool active: false property alias defaultFormatter: textEditor.defaultFormatter MultilineEditor { id: textEditor Layout.fillWidth: true Layout.fillHeight: true value: "" enabled: false showToolBar: false showSaveBtn: false showFormatters: false showValueSize: false objectName: "rdm_key_value_field" function validationRule(raw) { return true; } } onKeyTypeChanged: { textEditor.hintFormatter("JSON") } function initEmpty() { textEditor.initEmpty() } function getValue(validateVal, callback) { if (!validateVal) { return callback(true, {"value": ""}); } return textEditor.validate(function (valid, raw) { return callback(valid, {"value": raw}); }); } function setValue(rowValue) { if (!rowValue) return active = true textEditor.loadFormattedValue(rowValue['value']) } function isEdited() { return textEditor.isEdited } function reset() { textEditor.reset() active = false } } ================================================ FILE: src/qml/value-editor/editors/SingleItemEditor.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.1 import "." AbstractEditor { id: root anchors.fill: parent property bool active: false property alias defaultFormatter: textEditor.defaultFormatter MultilineEditor { id: textEditor Layout.fillWidth: true Layout.fillHeight: true value: "" enabled: root.active || root.state !== "edit" showToolBar: root.state == "edit" showSaveBtn: root.state == "edit" showFormatters: true showOnlyRWformatters: root.state == "add" || root.state == "new" objectName: "rdm_key_value_field" function validationRule(raw) { if (root.keyType === "string") return true; return qmlUtils.binaryStringLength(raw) > 0 } } onKeyTypeChanged: { if (root.keyType === "ReJSON") { textEditor.hintFormatter("JSON") } } function initEmpty() { textEditor.initEmpty() } function getValue(validateVal, callback) { if (!validateVal) { return callback(true, {"value": ""}); } return textEditor.validate(function (valid, raw) { return callback(valid, {"value": raw}); }); } function setValue(rowValue) { if (!rowValue) return active = true textEditor.loadFormattedValue(rowValue['value']) } function isEdited() { return textEditor.isEdited } function reset() { textEditor.reset() active = false } } ================================================ FILE: src/qml/value-editor/editors/SortedSetItemEditor.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.1 import "." import "../../common" AbstractEditor { id: root anchors.fill: parent property bool active: false property alias defaultFormatter: textArea.defaultFormatter BetterLabel { Layout.fillWidth: true text: qsTranslate("RESP", "Score") } BetterTextField { id: scoreText Layout.fillWidth: true Layout.minimumHeight: 28 text: "" enabled: root.active || root.state !== "edit" placeholderText: qsTranslate("RESP","Score") validator: DoubleValidator { locale: "C"; } // force point as decimal separator objectName: "rdm_key_zset_score_field" property bool isEdited: false onTextChanged: { scoreText.isEdited = true } function setValue(v) { text = Number(v) scoreText.isEdited = false } Connections { target: keyTab.keyModel ? keyTab.keyModel : null onValueUpdated: scoreText.isEdited = false } function reset() { text = "" } } MultilineEditor { id: textArea Layout.fillWidth: true Layout.fillHeight: true value: "" enabled: root.active || root.state !== "edit" showToolBar: root.state == "edit" showSaveBtn: root.state == "edit" showFormatters: root.state == "edit" objectName: "rdm_key_zset_text_field" } function initEmpty() { textArea.initEmpty() } function getValue(validateVal, callback) { if (!validateVal) { return callback(true, {"value": "", "score": scoreText.text}); } return textArea.validate(function (valid, raw) { callback(valid, {"value": raw, "score": scoreText.text}) }); } function setValue(rowValue) { if (!rowValue) return active = true scoreText.setValue(rowValue['score']) textArea.loadFormattedValue(rowValue['value']) } function isEdited() { return textArea.isEdited || scoreText.isEdited } function reset() { root.active = false scoreText.reset() textArea.reset() } } ================================================ FILE: src/qml/value-editor/editors/StreamItemEditor.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.1 import "." import "../../common" AbstractEditor { id: root anchors.fill: parent property bool active: false property alias defaultFormatter: textArea.defaultFormatter BetterLabel { Layout.fillWidth: true text: qsTranslate("RESP","ID") } BetterTextField { id: idValue Layout.fillWidth: true Layout.minimumHeight: 28 text: "" objectName: "rdm_key_stream_id_field" property string valueHash: "" enabled: root.active || root.state !== "edit" readOnly: root.state == "edit" function reset() { text = (root.state == "add" || root.state == "new")? "*" : "" } function isEdited() { return Qt.md5(text) != valueHash } function setValue(v) { valueHash = Qt.md5(v) text = v } function validate(callback) { return callback(text == "*" || text.indexOf("-") !== -1, text); } } MultilineEditor { id: textArea Layout.fillWidth: true Layout.fillHeight: true enabled: root.active || root.state !== "edit" showToolBar: root.state == "edit" showSaveBtn: root.state == "edit" showFormatters: root.state == "edit" objectName: "rdm_key_stream_text_field" fieldLabel: qsTranslate("RESP","Value (represented as JSON object)") + ":" function validationRule(raw) { try { var obj = JSON.parse(raw); return typeof obj === "object"; } catch (e) { console.log("Json parsing error:", e) return false } } } function initEmpty() { textArea.initEmpty() idValue.reset() } function getValue(validateVal, callback) { idValue.validate(function (keyTextValid, idVal) { if (!validateVal) { return callback(keyTextValid, {"value": "", "id": idVal}); } else { textArea.validate(function (textAreaValid, value) { return callback(keyTextValid && textAreaValid, {"value": value, "id": idVal}); }); } }); } function setValue(rowValue) { if (!rowValue) return active = true idValue.setValue(rowValue['id']) textArea.loadFormattedValue(rowValue['value']) textArea.readOnly = root.state == "edit" } function isEdited() { return textArea.isEdited || idValue.isEdited() } function reset() { textArea.reset() idValue.reset() active = false } } ================================================ FILE: src/qml/value-editor/editors/UnsupportedDataType.qml ================================================ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.1 import "./../../common" import "." AbstractEditor { id: root anchors.fill: parent property bool active: false property string keyType: "" Item { Layout.fillHeight: true Layout.fillWidth: true } BetterLabel { Layout.fillWidth: true wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter text: qsTranslate("RESP","Unsupported Redis Data type ") + keyType font.pixelSize: 16 } Loader { id: betaSupportIsAvailable Layout.fillWidth: true asynchronous: true source: keyType? "https://resp.app/qml/BetaModuleSupport.qml?app_version=" + Qt.application.version + "&platform=" + Qt.platform.os + "&module=" + encodeURIComponent(keyType) + "&t=" + Date.now() : "" } Item { Layout.fillHeight: true Layout.fillWidth: true } onKeyTypeChanged: { uiBlocker.visible = false; } function initEmpty() { } function isEdited() { return false } function reset() { active = false } } ================================================ FILE: src/qml/value-editor/editors/editor.js ================================================ function getSupportedKeyTypes() { return ["string", "list", "set", "zset", "hash", "stream"] } function getEditorByTypeString(keyType, writeOnly) { if (keyType === "string" || keyType === "hyperloglog" || keyType === "list" || keyType === "set" || keyType === "ReJSON") { return "./editors/SingleItemEditor.qml" } else if (keyType === "zset") { return "./editors/SortedSetItemEditor.qml" } else if (keyType === "hash") { return "./editors/HashItemEditor.qml" } else if (keyType === "stream") { return "./editors/StreamItemEditor.qml" } else if (keyType === "bf" || keyType === "cf") { if (writeOnly) { return "./editors/SingleItemEditor.qml" } else { return "./editors/ReadOnlySingleItemEditor.qml" } } else if (keyType) { return "./editors/UnsupportedDataType.qml" } } ================================================ FILE: src/qml/value-editor/editors/formatters/ValueFormatters.qml ================================================ import QtQuick 2.0 import QtQml.Models 2.13 import "./hexy.js" as Hexy import "../../../common/platformutils.js" as PlatformUtils ListModel { id: rootModel property int _jsonFormatterIndex: 3 function guessFormatter(val, isBinary) { if (isBinary) { return 1 } else { if (qmlUtils.isJSON(val)) { return _jsonFormatterIndex } else { return 0 } } } function getDefaultFormatter(isBinary) { if (isBinary) { return 1 } else { return 0 } } function getJSONFormatter() { return rootModel.get(_jsonFormatterIndex) } property var rwFormatters function getFormatterIndex(name) { for (var index=0; index < rootModel.count; ++index) { var formatter = get(index); if (formatter['name'] == name) { return index; } } return 0; } function onEmbeddedFormattersLoaded(result) { for (var indx in result) { var formatterName = result[indx][0]; var readOnly = result[indx][1]; var getFormatted = function (formatterName) { var r = function (raw, callback, context) { return embeddedFormattersManager.decode(formatterName, raw, function (response) { return callback(response[0], response[1], response[2], response[3]) }) } return r }; var getRaw = function (formatterName) { var r = function (formatted, callback, context) { return embeddedFormattersManager.encode(formatterName, formatted, function (response) { return callback(response[0], response[1]) }) } return r }; var isValid = function (formatterName, context) { var r = function (raw, callback) { return embeddedFormattersManager.isValid(formatterName, raw, function (response) { return callback(response[0]) }) } return r }; rootModel.append({'name': formatterName, 'type': "embedded",}) rootModel.setProperty(rootModel.count - 1, "getFormatted", getFormatted(formatterName)) rootModel.setProperty(rootModel.count - 1, "getRaw", getRaw(formatterName)) rootModel.setProperty(rootModel.count - 1, "isValid", isValid(formatterName)) rootModel.setProperty(rootModel.count - 1, "readOnly", readOnly) rootModel.setProperty(rootModel.count - 1, "keyTypes", "*") } console.log("Embedded formatters:", result); } function loadEmbeddedFormatters() { embeddedFormattersManager.loadFormattersModule(function (result) { console.log("Is Embedded formatters module loaded:", result) if (!result) { return; } embeddedFormattersManager.loadFormatters(function (result) { rootModel.onEmbeddedFormattersLoaded(result); }); }) } function loadExternalFormatters() { var nativeFormatters = formattersManager.getPlainList(); for (var index in nativeFormatters) { var formatter = nativeFormatters[index]; var formatterName = formatter["name"]; var formatterId = formatter["id"]; var readOnly = formatter["readOnly"]; var getFormatted = function (formatterId) { var r = function (raw, callback, context) { return formattersManager.decode(formatterId, raw, context, callback) } return r }; var getRaw = function (formatterId) { var r = function (formatted, callback, context) { return formattersManager.encode(formatterId, formatted, context, callback) } return r }; var isValid = function (formatterId, context) { var r = function (raw, callback) { return formattersManager.isValid(formatterId, raw, context, callback) } return r }; rootModel.append({'name': formatterName, 'type': "external"}) rootModel.setProperty(rootModel.count - 1, "getFormatted", getFormatted(formatterId)) rootModel.setProperty(rootModel.count - 1, "getRaw", getRaw(formatterId)) rootModel.setProperty(rootModel.count - 1, "isValid", isValid(formatterId)) rootModel.setProperty(rootModel.count - 1, "readOnly", readOnly) } } function updateRWFormatters() { var result = []; for (var index=0; index < rootModel.count; ++index) { var formatter = get(index); if (formatter['readOnly'] == false) { result.push(formatter); } } rwFormatters = result; } ListElement { property string name: "Plain Text" property string type: "buildin" property string readOnly: false property string keyTypes: "*" property var getFormatted: function (raw, callback) { return callback("", raw, false, "plain") } property var isValid: function (raw, callback) { return callback(true) } property var getRaw: function (formatted, callback) { return callback("", formatted) } } ListElement { property string name: "HEX" property string type: "buildin" property string readOnly: true property string keyTypes: "*" property var getFormatted: function (raw, callback, context) { return callback("", qmlUtils.printable(raw), false, "plain") } property var isValid: function (raw, callback, context) { return callback(true) } property var getRaw: function (formatted, callback, context) { return callback("", qmlUtils.printableToValue(formatted)) } } ListElement { property string name: "HEX TABLE" property string type: "buildin" property string readOnly: true property string keyTypes: "*" property var isValid: function (raw, callback, context) { return callback(true) } property var getFormatted: function (raw, callback, context) { return callback("", Hexy.hexy( qmlUtils.valueToBinary(raw), {'html': true, 'font': appSettings.valueEditorFont}), true, "html") } } ListElement { property string name: "JSON" property string type: "buildin" property string readOnly: false property string keyTypes: "*" property var getFormatted: function (raw, callback, context) { return callback("", qmlUtils.prettyPrintJSON(raw), false, "json") } property var isValid: function (raw, callback, context) { return callback(qmlUtils.isJSON(raw)) } property var getRaw: function (formatted, callback, context) { var minified = qmlUtils.minifyJSON(formatted); if (!minified) { return callback(qsTranslate("RESP", "Error") + ": Cannot minify JSON string") } else { return callback("", minified) } } } ListElement { property string name: "BASE64 to Text" property string type: "buildin" property string readOnly: true property string keyTypes: "*" property var getFormatted: function (raw, callback, context) { return callback("", Qt.atob(raw), false, "plain") } property var isValid: function (raw, callback, context) { return callback(true) } property var getRaw: function (formatted, callback, context) { return callback("", Qt.btoa(formatted)) } } ListElement { property string name: "BASE64 to JSON" property string type: "buildin" property string readOnly: true property string keyTypes: "*" property var getFormatted: function (raw, callback, context) { return callback("", Qt.atob(raw), false, "json") } property var isValid: function (raw, callback, context) { return callback(true) } property var getRaw: function (formatted, callback, context) { return callback("", Qt.btoa(formatted)) } } } ================================================ FILE: src/qml/value-editor/editors/formatters/hexy.js ================================================ // BASED ON: = hexy.js -- utility to create hex dumps // http://github.com/a2800276/hexy.js var hexy = function (buffer, config) { var h = new Hexy(buffer, config) return h.toString() } var Hexy = function (buffer, config) { var self = {} config = config || {} self.buffer = buffer // magic string conversion here? self.width = config.width || 16 self.numbering = config.numbering == "none" ? "none" : "hex_bytes" switch (config.format) { case "none": case "twos": self.format = config.format break default: self.format = "fours" } self.caps = config.caps == "upper" ? "upper" : "lower" self.annotate = config.annotate == "none" ? "none" : "ascii" self.prefix = config.prefix || "" self.indent = config.indent || 0 self.html = config.html || false self.offset = config.offset || 0 self.length = config.length || -1 self.display_offset = config.display_offset || 0 self.font = config.font || "monospace" if (self.offset) { if (self.offset < self.buffer.length) { self.buffer = self.buffer.slice(self.offset) } } if (self.length !== -1) { if (self.length <= self.buffer.length) { self.buffer = self.buffer.slice(0,self.length) } } for (var i = 0; i!=self.indent; ++i) { self.prefix = " "+self.prefix } this.toString = function () { var str = "" if (self.html) { str += "\n"} //split up into line of max `self.width` var line_arr = lines() //lines().forEach(function(hex_raw, i) for (var i = 0; i!= line_arr.length; ++i) { var hex_raw = line_arr[i], hex = hex_raw[0], raw = hex_raw[1] //insert spaces every `self.format.twos` or fours var howMany = hex.length if (self.format === "fours") { howMany = 4 } else if (self.format === "twos") { howMany = 2 } var hex_formatted = "" for (var j =0; j< hex.length; j+=howMany) { var s = hex.substr(j, howMany) hex_formatted += s + " " } var addr = (i*self.width)+self.offset+self.display_offset; if (self.html) { var odd = i%2 == 0 ? " even" : " odd" str += "" } str += self.prefix str += "\n" } else { str += "\n" } } if (self.html) { str += "
" if (self.numbering === "hex_bytes") { str += pad(addr, 8) // padding... str += ": " } str += "" var padlen = 0 switch(self.format) { case "eights": padlen = self.width*2 + Math.floor(self.width/4) break case "fours": padlen = self.width*2 + Math.floor(self.width/2) break case "twos": padlen = self.width*3 + 2 break default: padlen = self.width * 2 + 1 } str += rpad(hex_formatted, padlen) str += "" if (self.annotate === "ascii") { str+=" " str += escape(raw.replace(/[\000-\040\177-\377]/g, ".")) } if (self.html) { str += "
\n"} return str } var lines = function() { var hex_raw = [] for (var i = 0; i= self.buffer.length ? self.buffer.length : i+self.width, slice = self.buffer.slice(begin, end), hex = self.caps === "upper" ? hexu(slice) : hexl(slice), raw = String.fromCharCode.apply(null, slice) hex_raw.push([hex,raw]) } return hex_raw } var hexl = function (buffer) { var str = "" for (var i=0; i!=buffer.length; ++i) { if (buffer.constructor == String) { str += pad(buffer.charCodeAt(i), 2) } else { str += pad(buffer[i], 2) } } return str } var hexu = function (buffer) { return hexl(buffer).toUpperCase() } var pad = function(b, len) { var s = b.toString(16) while (s.length < len) { s = "0" + s } return s } var rpad = function(s, len) { for (var n = len - s.length; n>0; --n) { if (self.html) { s += " " } else { s += " " } } return s } var escape = function (str) { str = str.split("&").join("&") str = str.split("<").join("<") str = str.split(">").join(">") return str } } ================================================ FILE: src/qml/value-editor/filters/ListFilters.qml ================================================ import QtQuick 2.13 import QtQuick.Layouts 1.1 import "./../../common" RowLayout { BetterLabel { text: qsTranslate("RESP", "Order of elements:") } BetterComboBox { id: filterDirection enabled: !keyTab.keyModel.singlePageMode ListModel { id: filterDirectionModel Component.onCompleted: { filterDirectionModel.append({ value: "default", text: qsTranslate("RESP", "Default") }) filterDirectionModel.append({ value: "reverse", text: qsTranslate("RESP", "Reverse") }) filterDirection.currentIndex = 0 } } textRole: "text" model: filterDirectionModel onCurrentIndexChanged: { var direction = filterDirectionModel.get(currentIndex)["value"]; keyModel.setFilter("order", direction); reloadValue(); } } Item { Layout.fillWidth: true } } ================================================ FILE: src/qml/value-editor/filters/StreamFilters.qml ================================================ import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import "./../../common" import "../../common/platformutils.js" as PlatformUtils RowLayout { id: streamFilter objectName: "rdm_stream_filter" property bool enabled: streamRangeSlider.from < streamRangeSlider.to property string dateTimeFormat: PlatformUtils.dateTimeFormat property string inputMask: "9999-99-99 99:99:99.999" function setStreamFilter() { var start = Date.fromLocaleString(locale, streamRangeStartField.text, streamFilter.dateTimeFormat).getTime() var end = Date.fromLocaleString(locale, streamRangeEndField.text, streamFilter.dateTimeFormat).getTime() if (start < end) { keyTab.keyModel.setFilter("start", start) keyTab.keyModel.setFilter("end", end) reloadValue() streamRangeStartField.isEdited = false; streamRangeEndField.isEdited = false; } else { notification.showError(qsTranslate("RESP","Start date should be less than End date")) } } RegExpValidator { id: dateTimeValidator regExp: /(\d{4})-(\d{2})-(\d{2}) (\d{2})\:(\d{2})\:(\d{2}).(\d{3})/ } BetterTextField { id: streamRangeStartField objectName: "rdm_stream_filter_start_field" property bool isEdited: false implicitWidth: 180 font.pixelSize: 14 color: enabled ? sysPalette.text : disabledSysPalette.text enabled: streamFilter.enabled text: new Date(streamRangeSlider.first.value).toLocaleString(locale, streamFilter.dateTimeFormat) inputMask: streamFilter.inputMask validator: dateTimeValidator BetterToolTip { title: streamRangeSlider.first.value visible: title && (streamRangeSlider.first.pressed || streamRangeSlider.first.hovered) } onTextEdited: { isEdited = true } onAccepted: { streamFilter.setStreamFilter() } } RangeSlider { id: streamRangeSlider objectName: "rdm_stream_filter_range_slider" implicitWidth: 100 Layout.fillWidth: true palette.midlight: sysPalette.button palette.dark: enabled ? sysPalette.highlight : disabledSysPalette.highlight padding: 0 enabled: streamFilter.enabled stepSize: 1.0 snapMode: Slider.SnapAlways first.handle.implicitWidth: PlatformUtils.isOSXRetina(Screen) ? 15 : 20 first.handle.implicitHeight: PlatformUtils.isOSXRetina(Screen) ? 15 : 20 second.handle.implicitWidth: PlatformUtils.isOSXRetina(Screen) ? 15 : 20 second.handle.implicitHeight: PlatformUtils.isOSXRetina(Screen) ? 15 : 20 first.onPressedChanged: { if (!first.pressed) { streamRangeStartField.isEdited = true } } second.onPressedChanged: { if (!second.pressed) { streamRangeEndField.isEdited = true } } } BetterTextField { id: streamRangeEndField objectName: "rdm_stream_filter_end_field" property bool isEdited: false implicitWidth: 180 font.pixelSize: 14 color: enabled ? sysPalette.text : disabledSysPalette.text enabled: streamFilter.enabled text: new Date(streamRangeSlider.second.value).toLocaleString(locale, streamFilter.dateTimeFormat) inputMask: streamFilter.inputMask validator: dateTimeValidator BetterToolTip { title: streamRangeSlider.second.value visible: title && (streamRangeSlider.second.pressed || streamRangeSlider.second.hovered) } onTextEdited: { isEdited = true } onAccepted: { streamFilter.setStreamFilter() } } BetterButton { objectName: "rdm_stream_filter_apply_btn" implicitWidth: 30 iconSource: PlatformUtils.getThemeIcon("filter.svg") tooltip: qsTranslate("RESP","Apply filter") enabled: (streamRangeStartField.isEdited || streamRangeEndField.isEdited) && streamFilter.enabled onClicked: { streamFilter.setStreamFilter() } } Connections { target: keyModel ? keyModel : null function onRowsLoaded() { var firstEntry = String(keyModel.filter("first-entry")).slice(0, -2) var lastEntry = String(keyModel.filter("last-entry")).slice(0, -2) var start = keyModel.filter("start") ? keyModel.filter("start") : firstEntry var end = keyModel.filter("end") ? keyModel.filter("end") : lastEntry console.log("STREAM filter start end:", start, end) streamRangeSlider.from = Number(firstEntry) streamRangeSlider.to = Number(lastEntry) streamRangeSlider.first.value = Number(start) streamRangeSlider.second.value = Number(end) } } } ================================================ FILE: src/resources/Info.plist.sample ================================================ CFBundleDevelopmentRegion en-US CFBundleDisplayName RESP CFBundleExecutable RESP CFBundleIconFile logo.icns CFBundleIdentifier com.redisdesktop.rdm CFBundleInfoDictionaryVersion 6.0 CFBundleName RESP CFBundlePackageType APPL CFBundleShortVersionString 0.0.0 CFBundleVersion 0.0.0 NSHumanReadableCopyright © 2013-2019, Igor Malinovskiy. NSPrincipalClass NSApplication NSHighResolutionCapable CFBundleSignature ???? NSSupportsAutomaticGraphicsSwitching LSApplicationCategoryType public.app-category.developer-tools CFBundleSupportedPlatforms MacOSX LSMinimumSystemVersion 10.14.0 ================================================ FILE: src/resources/commands.json ================================================ [{"cmd": "ACL", "summary": "A container for Access List Control commands ", "arguments": "", "since": "6.0.0"}, {"cmd": "ACL CAT", "summary": "List the ACL categories or the commands inside a category", "arguments": "[categoryname]", "since": "6.0.0"}, {"cmd": "ACL DELUSER", "summary": "Remove the specified ACL users and the associated rules", "arguments": "username", "since": "6.0.0"}, {"cmd": "ACL GENPASS", "summary": "Generate a pseudorandom secure password to use for ACL users", "arguments": "[bits]", "since": "6.0.0"}, {"cmd": "ACL GETUSER", "summary": "Get the rules for a specific ACL user", "arguments": "username", "since": "6.0.0"}, {"cmd": "ACL HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "6.0.0"}, {"cmd": "ACL LIST", "summary": "List the current ACL rules in ACL config file format", "arguments": "", "since": "6.0.0"}, {"cmd": "ACL LOAD", "summary": "Reload the ACLs from the configured ACL file", "arguments": "", "since": "6.0.0"}, {"cmd": "ACL LOG", "summary": "List latest events denied because of ACLs in place", "arguments": "[operation]", "since": "6.0.0"}, {"cmd": "ACL SAVE", "summary": "Save the current ACL rules in the configured ACL file", "arguments": "", "since": "6.0.0"}, {"cmd": "ACL SETUSER", "summary": "Modify or create the rules for a specific ACL user", "arguments": "username [rule]", "since": "6.0.0"}, {"cmd": "ACL USERS", "summary": "List the username of all the configured ACL rules", "arguments": "", "since": "6.0.0"}, {"cmd": "ACL WHOAMI", "summary": "Return the name of the user associated to the current connection", "arguments": "", "since": "6.0.0"}, {"cmd": "APPEND", "summary": "Append a value to a key", "arguments": "key value", "since": "2.0.0"}, {"cmd": "ASKING", "summary": "Sent by cluster clients after an -ASK redirect", "arguments": "", "since": "3.0.0"}, {"cmd": "AUTH", "summary": "Authenticate to the server", "arguments": "[username] password", "since": "1.0.0"}, {"cmd": "BGREWRITEAOF", "summary": "Asynchronously rewrite the append-only file", "arguments": "", "since": "1.0.0"}, {"cmd": "BGSAVE", "summary": "Asynchronously save the dataset to disk", "arguments": "[schedule]", "since": "1.0.0"}, {"cmd": "BITCOUNT", "summary": "Count set bits in a string", "arguments": "key [index]", "since": "2.6.0"}, {"cmd": "BITFIELD", "summary": "Perform arbitrary bitfield integer operations on strings", "arguments": "key [encoding_offset] [encoding_offset_value] [encoding_offset_increment] [wrap_sat_fail]", "since": "3.2.0"}, {"cmd": "BITFIELD_RO", "summary": "Perform arbitrary bitfield integer operations on strings. Read-only variant of BITFIELD", "arguments": "key encoding_offset", "since": "6.2.0"}, {"cmd": "BITOP", "summary": "Perform bitwise operations between strings", "arguments": "operation destkey key", "since": "2.6.0"}, {"cmd": "BITPOS", "summary": "Find first bit set or clear in a string", "arguments": "key bit [index]", "since": "2.8.7"}, {"cmd": "BLMOVE", "summary": "Pop an element from a list, push it to another list and return it; or block until one is available", "arguments": "source destination wherefrom whereto timeout", "since": "6.2.0"}, {"cmd": "BLMPOP", "summary": "Pop elements from a list, or block until one is available", "arguments": "timeout numkeys key where [count]", "since": "7.0.0"}, {"cmd": "BLPOP", "summary": "Remove and get the first element in a list, or block until one is available", "arguments": "key timeout", "since": "2.0.0"}, {"cmd": "BRPOP", "summary": "Remove and get the last element in a list, or block until one is available", "arguments": "key timeout", "since": "2.0.0"}, {"cmd": "BRPOPLPUSH", "summary": "Pop an element from a list, push it to another list and return it; or block until one is available", "arguments": "source destination timeout", "since": "2.2.0"}, {"cmd": "BZMPOP", "summary": "Remove and return members with scores in a sorted set or block until one is available", "arguments": "timeout numkeys key where [count]", "since": "7.0.0"}, {"cmd": "BZPOPMAX", "summary": "Remove and return the member with the highest score from one or more sorted sets, or block until one is available", "arguments": "key timeout", "since": "5.0.0"}, {"cmd": "BZPOPMIN", "summary": "Remove and return the member with the lowest score from one or more sorted sets, or block until one is available", "arguments": "key timeout", "since": "5.0.0"}, {"cmd": "CLIENT", "summary": "A container for client connection commands", "arguments": "", "since": "2.4.0"}, {"cmd": "CLIENT CACHING", "summary": "Instruct the server about tracking or not keys in the next request", "arguments": "mode", "since": "6.0.0"}, {"cmd": "CLIENT GETNAME", "summary": "Get the current connection name", "arguments": "", "since": "2.6.9"}, {"cmd": "CLIENT GETREDIR", "summary": "Get tracking notifications redirection client ID if any", "arguments": "", "since": "6.0.0"}, {"cmd": "CLIENT HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "5.0.0"}, {"cmd": "CLIENT ID", "summary": "Returns the client ID for the current connection", "arguments": "", "since": "5.0.0"}, {"cmd": "CLIENT INFO", "summary": "Returns information about the current client connection.", "arguments": "", "since": "6.2.0"}, {"cmd": "CLIENT KILL", "summary": "Kill the connection of a client", "arguments": "[ip:port] [client-id] [normal_master_slave_pubsub] [username] [ip:port] [ip:port] [yes/no]", "since": "2.4.0"}, {"cmd": "CLIENT LIST", "summary": "Get the list of client connections", "arguments": "[normal_master_replica_pubsub] [id]", "since": "2.4.0"}, {"cmd": "CLIENT NO-EVICT", "summary": "Set client eviction mode for the current connection", "arguments": "enabled", "since": "7.0.0"}, {"cmd": "CLIENT PAUSE", "summary": "Stop processing commands from clients for some time", "arguments": "timeout [mode]", "since": "2.9.50"}, {"cmd": "CLIENT REPLY", "summary": "Instruct the server whether to reply to commands", "arguments": "on_off_skip", "since": "3.2.0"}, {"cmd": "CLIENT SETNAME", "summary": "Set the current connection name", "arguments": "connection-name", "since": "2.6.9"}, {"cmd": "CLIENT TRACKING", "summary": "Enable or disable server assisted client side caching support", "arguments": "status [client-id] [prefix] [bcast] [optin] [optout] [noloop]", "since": "6.0.0"}, {"cmd": "CLIENT TRACKINGINFO", "summary": "Return information about server assisted client side caching for the current connection", "arguments": "", "since": "6.2.0"}, {"cmd": "CLIENT UNBLOCK", "summary": "Unblock a client blocked in a blocking command from a different connection", "arguments": "client-id [timeout_error]", "since": "5.0.0"}, {"cmd": "CLIENT UNPAUSE", "summary": "Resume processing of clients that were paused", "arguments": "", "since": "6.2.0"}, {"cmd": "CLUSTER", "summary": "A container for cluster commands", "arguments": "", "since": "3.0.0"}, {"cmd": "CLUSTER ADDSLOTS", "summary": "Assign new hash slots to receiving node", "arguments": "slot", "since": "3.0.0"}, {"cmd": "CLUSTER ADDSLOTSRANGE", "summary": "Assign new hash slots to receiving node", "arguments": "start-slot_end-slot", "since": "7.0.0"}, {"cmd": "CLUSTER BUMPEPOCH", "summary": "Advance the cluster config epoch", "arguments": "", "since": "3.0.0"}, {"cmd": "CLUSTER COUNT-FAILURE-REPORTS", "summary": "Return the number of failure reports active for a given node", "arguments": "node-id", "since": "3.0.0"}, {"cmd": "CLUSTER COUNTKEYSINSLOT", "summary": "Return the number of local keys in the specified hash slot", "arguments": "slot", "since": "3.0.0"}, {"cmd": "CLUSTER DELSLOTS", "summary": "Set hash slots as unbound in receiving node", "arguments": "slot", "since": "3.0.0"}, {"cmd": "CLUSTER DELSLOTSRANGE", "summary": "Set hash slots as unbound in receiving node", "arguments": "start-slot_end-slot", "since": "7.0.0"}, {"cmd": "CLUSTER FAILOVER", "summary": "Forces a replica to perform a manual failover of its master.", "arguments": "[options]", "since": "3.0.0"}, {"cmd": "CLUSTER FLUSHSLOTS", "summary": "Delete a node's own slots information", "arguments": "", "since": "3.0.0"}, {"cmd": "CLUSTER FORGET", "summary": "Remove a node from the nodes table", "arguments": "node-id", "since": "3.0.0"}, {"cmd": "CLUSTER GETKEYSINSLOT", "summary": "Return local key names in the specified hash slot", "arguments": "slot count", "since": "3.0.0"}, {"cmd": "CLUSTER HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "5.0.0"}, {"cmd": "CLUSTER INFO", "summary": "Provides info about Redis Cluster node state", "arguments": "", "since": "3.0.0"}, {"cmd": "CLUSTER KEYSLOT", "summary": "Returns the hash slot of the specified key", "arguments": "key", "since": "3.0.0"}, {"cmd": "CLUSTER LINKS", "summary": "Returns a list of all TCP links to and from peer nodes in cluster", "arguments": "", "since": "7.0.0"}, {"cmd": "CLUSTER MEET", "summary": "Force a node cluster to handshake with another node", "arguments": "ip port", "since": "3.0.0"}, {"cmd": "CLUSTER MYID", "summary": "Return the node id", "arguments": "", "since": "3.0.0"}, {"cmd": "CLUSTER NODES", "summary": "Get Cluster config for the node", "arguments": "", "since": "3.0.0"}, {"cmd": "CLUSTER REPLICAS", "summary": "List replica nodes of the specified master node", "arguments": "node-id", "since": "5.0.0"}, {"cmd": "CLUSTER REPLICATE", "summary": "Reconfigure a node as a replica of the specified master node", "arguments": "node-id", "since": "3.0.0"}, {"cmd": "CLUSTER RESET", "summary": "Reset a Redis Cluster node", "arguments": "[hard_soft]", "since": "3.0.0"}, {"cmd": "CLUSTER SAVECONFIG", "summary": "Forces the node to save cluster state on disk", "arguments": "", "since": "3.0.0"}, {"cmd": "CLUSTER SET-CONFIG-EPOCH", "summary": "Set the configuration epoch in a new node", "arguments": "config-epoch", "since": "3.0.0"}, {"cmd": "CLUSTER SETSLOT", "summary": "Bind a hash slot to a specific node", "arguments": "slot subcommand", "since": "3.0.0"}, {"cmd": "CLUSTER SLAVES", "summary": "List replica nodes of the specified master node", "arguments": "node-id", "since": "3.0.0"}, {"cmd": "CLUSTER SLOTS", "summary": "Get array of Cluster slot to node mappings", "arguments": "", "since": "3.0.0"}, {"cmd": "COMMAND", "summary": "Get array of Redis command details", "arguments": "", "since": "2.8.13"}, {"cmd": "COMMAND COUNT", "summary": "Get total number of Redis commands", "arguments": "", "since": "2.8.13"}, {"cmd": "COMMAND GETKEYS", "summary": "Extract keys given a full Redis command", "arguments": "", "since": "2.8.13"}, {"cmd": "COMMAND HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "5.0.0"}, {"cmd": "COMMAND INFO", "summary": "Get array of specific Redis command details", "arguments": "command-name", "since": "2.8.13"}, {"cmd": "COMMAND LIST", "summary": "Get an array of Redis command names", "arguments": "[filterby]", "since": "7.0.0"}, {"cmd": "CONFIG", "summary": "A container for server configuration commands", "arguments": "", "since": "2.0.0"}, {"cmd": "CONFIG GET", "summary": "Get the values of configuration parameters", "arguments": "parameter", "since": "2.0.0"}, {"cmd": "CONFIG HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "5.0.0"}, {"cmd": "CONFIG RESETSTAT", "summary": "Reset the stats returned by INFO", "arguments": "", "since": "2.0.0"}, {"cmd": "CONFIG REWRITE", "summary": "Rewrite the configuration file with the in memory configuration", "arguments": "", "since": "2.8.0"}, {"cmd": "CONFIG SET", "summary": "Set configuration parameters to the given values", "arguments": "parameter_value", "since": "2.0.0"}, {"cmd": "COPY", "summary": "Copy a key", "arguments": "source destination [destination-db] [replace]", "since": "6.2.0"}, {"cmd": "DBSIZE", "summary": "Return the number of keys in the selected database", "arguments": "", "since": "1.0.0"}, {"cmd": "DEBUG", "summary": "A container for debugging commands", "arguments": "", "since": "1.0.0"}, {"cmd": "DECR", "summary": "Decrement the integer value of a key by one", "arguments": "key", "since": "1.0.0"}, {"cmd": "DECRBY", "summary": "Decrement the integer value of a key by the given number", "arguments": "key decrement", "since": "1.0.0"}, {"cmd": "DEL", "summary": "Delete a key", "arguments": "key", "since": "1.0.0"}, {"cmd": "DISCARD", "summary": "Discard all commands issued after MULTI", "arguments": "", "since": "2.0.0"}, {"cmd": "DUMP", "summary": "Return a serialized version of the value stored at the specified key.", "arguments": "key", "since": "2.6.0"}, {"cmd": "ECHO", "summary": "Echo the given string", "arguments": "message", "since": "1.0.0"}, {"cmd": "EVAL", "summary": "Execute a Lua script server side", "arguments": "script numkeys [key] [arg]", "since": "2.6.0"}, {"cmd": "EVALSHA", "summary": "Execute a Lua script server side", "arguments": "sha1 numkeys [key] [arg]", "since": "2.6.0"}, {"cmd": "EVALSHA_RO", "summary": "Execute a read-only Lua script server side", "arguments": "sha1 numkeys key arg", "since": "7.0.0"}, {"cmd": "EVAL_RO", "summary": "Execute a read-only Lua script server side", "arguments": "script numkeys key arg", "since": "7.0.0"}, {"cmd": "EXEC", "summary": "Execute all commands issued after MULTI", "arguments": "", "since": "1.2.0"}, {"cmd": "EXISTS", "summary": "Determine if a key exists", "arguments": "key", "since": "1.0.0"}, {"cmd": "EXPIRE", "summary": "Set a key's time to live in seconds", "arguments": "key seconds [condition]", "since": "1.0.0"}, {"cmd": "EXPIREAT", "summary": "Set the expiration for a key as a UNIX timestamp", "arguments": "key timestamp [condition]", "since": "1.2.0"}, {"cmd": "EXPIRETIME", "summary": "Get the expiration Unix timestamp for a key", "arguments": "key", "since": "7.0.0"}, {"cmd": "FAILOVER", "summary": "Start a coordinated failover between this server and one of its replicas.", "arguments": "[target] [abort] [milliseconds]", "since": "6.2.0"}, {"cmd": "FCALL", "summary": "PATCH__TBD__38__", "arguments": "function numkeys key arg", "since": "7.0.0"}, {"cmd": "FCALL_RO", "summary": "PATCH__TBD__7__", "arguments": "function numkeys key arg", "since": "7.0.0"}, {"cmd": "FLUSHALL", "summary": "Remove all keys from all databases", "arguments": "[async]", "since": "1.0.0"}, {"cmd": "FLUSHDB", "summary": "Remove all keys from the current database", "arguments": "[async]", "since": "1.0.0"}, {"cmd": "FUNCTION", "summary": "A container for function commands", "arguments": "", "since": "7.0.0"}, {"cmd": "FUNCTION CREATE", "summary": "Create a function with the given arguments (name, code, description)", "arguments": "engine-name function-name [replace] [function-description] function-code", "since": "7.0.0"}, {"cmd": "FUNCTION DELETE", "summary": "Delete a function by name", "arguments": "function-name", "since": "7.0.0"}, {"cmd": "FUNCTION DUMP", "summary": "Dump all functions into a serialized binary payload", "arguments": "", "since": "7.0.0"}, {"cmd": "FUNCTION FLUSH", "summary": "Deleting all functions", "arguments": "[async]", "since": "7.0.0"}, {"cmd": "FUNCTION HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "7.0.0"}, {"cmd": "FUNCTION INFO", "summary": "Return information about a function by function name", "arguments": "function-name [withcode]", "since": "7.0.0"}, {"cmd": "FUNCTION KILL", "summary": "Kill the function currently in execution.", "arguments": "", "since": "7.0.0"}, {"cmd": "FUNCTION LIST", "summary": "List information about all the functions", "arguments": "", "since": "7.0.0"}, {"cmd": "FUNCTION RESTORE", "summary": "Restore all the functions on the given payload", "arguments": "serialized-value [policy]", "since": "7.0.0"}, {"cmd": "FUNCTION STATS", "summary": "Return information about the function currently running (name, description, duration)", "arguments": "", "since": "7.0.0"}, {"cmd": "GEOADD", "summary": "Add one or more geospatial items in the geospatial index represented using a sorted set", "arguments": "key [condition] [change] longitude_latitude_member", "since": "3.2.0"}, {"cmd": "GEODIST", "summary": "Returns the distance between two members of a geospatial index", "arguments": "key member1 member2 [unit]", "since": "3.2.0"}, {"cmd": "GEOHASH", "summary": "Returns members of a geospatial index as standard geohash strings", "arguments": "key member", "since": "3.2.0"}, {"cmd": "GEOPOS", "summary": "Returns longitude and latitude of members of a geospatial index", "arguments": "key member", "since": "3.2.0"}, {"cmd": "GEORADIUS", "summary": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", "arguments": "key longitude latitude radius unit [withcoord] [withdist] [withhash] [count] [order] [key] [key]", "since": "3.2.0"}, {"cmd": "GEORADIUSBYMEMBER", "summary": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", "arguments": "key member radius unit [withcoord] [withdist] [withhash] [count] [order] [key] [key]", "since": "3.2.0"}, {"cmd": "GEORADIUSBYMEMBER_RO", "summary": "A read-only variant for GEORADIUSBYMEMBER", "arguments": "key member radius unit [withcoord] [withdist] [withhash] [count] [order]", "since": "3.2.10"}, {"cmd": "GEORADIUS_RO", "summary": "A read-only variant for GEORADIUS", "arguments": "key longitude latitude radius unit [withcoord] [withdist] [withhash] [count] [order]", "since": "3.2.10"}, {"cmd": "GEOSEARCH", "summary": "Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle.", "arguments": "key [member] [longitude_latitude] [circle] [box] [order] [count] [withcoord] [withdist] [withhash]", "since": "6.2.0"}, {"cmd": "GEOSEARCHSTORE", "summary": "Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle, and store the result in another key.", "arguments": "destination source [member] [longitude_latitude] [circle] [box] [order] [count] [storedist]", "since": "6.2.0"}, {"cmd": "GET", "summary": "Get the value of a key", "arguments": "key", "since": "1.0.0"}, {"cmd": "GETBIT", "summary": "Returns the bit value at offset in the string value stored at key", "arguments": "key offset", "since": "2.2.0"}, {"cmd": "GETDEL", "summary": "Get the value of a key and delete the key", "arguments": "key", "since": "6.2.0"}, {"cmd": "GETEX", "summary": "Get the value of a key and optionally set its expiration", "arguments": "key [expiration]", "since": "6.2.0"}, {"cmd": "GETRANGE", "summary": "Get a substring of the string stored at a key", "arguments": "key start end", "since": "2.4.0"}, {"cmd": "GETSET", "summary": "Set the string value of a key and return its old value", "arguments": "key value", "since": "1.0.0"}, {"cmd": "HDEL", "summary": "Delete one or more hash fields", "arguments": "key field", "since": "2.0.0"}, {"cmd": "HELLO", "summary": "Handshake with Redis", "arguments": "[arguments]", "since": "6.0.0"}, {"cmd": "HEXISTS", "summary": "Determine if a hash field exists", "arguments": "key field", "since": "2.0.0"}, {"cmd": "HGET", "summary": "Get the value of a hash field", "arguments": "key field", "since": "2.0.0"}, {"cmd": "HGETALL", "summary": "Get all the fields and values in a hash", "arguments": "key", "since": "2.0.0"}, {"cmd": "HINCRBY", "summary": "Increment the integer value of a hash field by the given number", "arguments": "key field increment", "since": "2.0.0"}, {"cmd": "HINCRBYFLOAT", "summary": "Increment the float value of a hash field by the given amount", "arguments": "key field increment", "since": "2.6.0"}, {"cmd": "HKEYS", "summary": "Get all the fields in a hash", "arguments": "key", "since": "2.0.0"}, {"cmd": "HLEN", "summary": "Get the number of fields in a hash", "arguments": "key", "since": "2.0.0"}, {"cmd": "HMGET", "summary": "Get the values of all the given hash fields", "arguments": "key field", "since": "2.0.0"}, {"cmd": "HMSET", "summary": "Set multiple hash fields to multiple values", "arguments": "key field_value", "since": "2.0.0"}, {"cmd": "HRANDFIELD", "summary": "Get one or multiple random fields from a hash", "arguments": "key [options]", "since": "6.2.0"}, {"cmd": "HSCAN", "summary": "Incrementally iterate hash fields and associated values", "arguments": "key cursor [pattern] [count]", "since": "2.8.0"}, {"cmd": "HSET", "summary": "Set the string value of a hash field", "arguments": "key field_value", "since": "2.0.0"}, {"cmd": "HSETNX", "summary": "Set the value of a hash field, only if the field does not exist", "arguments": "key field value", "since": "2.0.0"}, {"cmd": "HSTRLEN", "summary": "Get the length of the value of a hash field", "arguments": "key field", "since": "3.2.0"}, {"cmd": "HVALS", "summary": "Get all the values in a hash", "arguments": "key", "since": "2.0.0"}, {"cmd": "INCR", "summary": "Increment the integer value of a key by one", "arguments": "key", "since": "1.0.0"}, {"cmd": "INCRBY", "summary": "Increment the integer value of a key by the given amount", "arguments": "key increment", "since": "1.0.0"}, {"cmd": "INCRBYFLOAT", "summary": "Increment the float value of a key by the given amount", "arguments": "key increment", "since": "2.6.0"}, {"cmd": "INFO", "summary": "Get information and statistics about the server", "arguments": "[section]", "since": "1.0.0"}, {"cmd": "KEYS", "summary": "Find all keys matching the given pattern", "arguments": "pattern", "since": "1.0.0"}, {"cmd": "LASTSAVE", "summary": "Get the UNIX time stamp of the last successful save to disk", "arguments": "", "since": "1.0.0"}, {"cmd": "LATENCY", "summary": "A container for latency diagnostics commands", "arguments": "", "since": "2.8.13"}, {"cmd": "LATENCY DOCTOR", "summary": "Return a human readable latency analysis report.", "arguments": "", "since": "2.8.13"}, {"cmd": "LATENCY GRAPH", "summary": "Return a latency graph for the event.", "arguments": "event", "since": "2.8.13"}, {"cmd": "LATENCY HELP", "summary": "Show helpful text about the different subcommands.", "arguments": "", "since": "2.8.13"}, {"cmd": "LATENCY HISTORY", "summary": "Return timestamp-latency samples for the event.", "arguments": "event", "since": "2.8.13"}, {"cmd": "LATENCY LATEST", "summary": "Return the latest latency samples for all events.", "arguments": "", "since": "2.8.13"}, {"cmd": "LATENCY RESET", "summary": "Reset latency data for one or more events.", "arguments": "[event]", "since": "2.8.13"}, {"cmd": "LCS", "summary": "Find longest common substring", "arguments": "key1 key2 [len] [idx] [len] [withmatchlen]", "since": "7.0.0"}, {"cmd": "LINDEX", "summary": "Get an element from a list by its index", "arguments": "key index", "since": "1.0.0"}, {"cmd": "LINSERT", "summary": "Insert an element before or after another element in a list", "arguments": "key where pivot element", "since": "2.2.0"}, {"cmd": "LLEN", "summary": "Get the length of a list", "arguments": "key", "since": "1.0.0"}, {"cmd": "LMOVE", "summary": "Pop an element from a list, push it to another list and return it", "arguments": "source destination wherefrom whereto", "since": "6.2.0"}, {"cmd": "LMPOP", "summary": "Pop elements from a list", "arguments": "numkeys key where [count]", "since": "7.0.0"}, {"cmd": "LOLWUT", "summary": "Display some computer art and the Redis version", "arguments": "[version]", "since": "5.0.0"}, {"cmd": "LPOP", "summary": "Remove and get the first elements in a list", "arguments": "key [count]", "since": "1.0.0"}, {"cmd": "LPOS", "summary": "Return the index of matching elements on a list", "arguments": "key element [rank] [num-matches] [len]", "since": "6.0.6"}, {"cmd": "LPUSH", "summary": "Prepend one or multiple elements to a list", "arguments": "key element", "since": "1.0.0"}, {"cmd": "LPUSHX", "summary": "Prepend an element to a list, only if the list exists", "arguments": "key element", "since": "2.2.0"}, {"cmd": "LRANGE", "summary": "Get a range of elements from a list", "arguments": "key start stop", "since": "1.0.0"}, {"cmd": "LREM", "summary": "Remove elements from a list", "arguments": "key count element", "since": "1.0.0"}, {"cmd": "LSET", "summary": "Set the value of an element in a list by its index", "arguments": "key index element", "since": "1.0.0"}, {"cmd": "LTRIM", "summary": "Trim a list to the specified range", "arguments": "key start stop", "since": "1.0.0"}, {"cmd": "MEMORY", "summary": "A container for memory diagnostics commands", "arguments": "", "since": "4.0.0"}, {"cmd": "MEMORY DOCTOR", "summary": "Outputs memory problems report", "arguments": "", "since": "4.0.0"}, {"cmd": "MEMORY HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "4.0.0"}, {"cmd": "MEMORY MALLOC-STATS", "summary": "Show allocator internal stats", "arguments": "", "since": "4.0.0"}, {"cmd": "MEMORY PURGE", "summary": "Ask the allocator to release memory", "arguments": "", "since": "4.0.0"}, {"cmd": "MEMORY STATS", "summary": "Show memory usage details", "arguments": "", "since": "4.0.0"}, {"cmd": "MEMORY USAGE", "summary": "Estimate the memory usage of a key", "arguments": "key [count]", "since": "4.0.0"}, {"cmd": "MGET", "summary": "Get the values of all the given keys", "arguments": "key", "since": "1.0.0"}, {"cmd": "MIGRATE", "summary": "Atomically transfer a key from a Redis instance to another one.", "arguments": "host port key_or_empty_string destination-db timeout [copy] [replace] [password] [username_password] [key]", "since": "2.6.0"}, {"cmd": "MODULE", "summary": "A container for module commands", "arguments": "", "since": "4.0.0"}, {"cmd": "MODULE HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "5.0.0"}, {"cmd": "MODULE LIST", "summary": "List all modules loaded by the server", "arguments": "", "since": "4.0.0"}, {"cmd": "MODULE LOAD", "summary": "Load a module", "arguments": "path [arg]", "since": "4.0.0"}, {"cmd": "MODULE UNLOAD", "summary": "Unload a module", "arguments": "name", "since": "4.0.0"}, {"cmd": "MONITOR", "summary": "Listen for all requests received by the server in real time", "arguments": "", "since": "1.0.0"}, {"cmd": "MOVE", "summary": "Move a key to another database", "arguments": "key db", "since": "1.0.0"}, {"cmd": "MSET", "summary": "Set multiple keys to multiple values", "arguments": "key_value", "since": "1.0.1"}, {"cmd": "MSETNX", "summary": "Set multiple keys to multiple values, only if none of the keys exist", "arguments": "key_value", "since": "1.0.1"}, {"cmd": "MULTI", "summary": "Mark the start of a transaction block", "arguments": "", "since": "1.2.0"}, {"cmd": "OBJECT", "summary": "A container for object introspection commands", "arguments": "", "since": "2.2.3"}, {"cmd": "OBJECT ENCODING", "summary": "Inspect the internal encoding of a Redis object", "arguments": "key", "since": "2.2.3"}, {"cmd": "OBJECT FREQ", "summary": "Get the logarithmic access frequency counter of a Redis object", "arguments": "key", "since": "4.0.0"}, {"cmd": "OBJECT HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "6.2.0"}, {"cmd": "OBJECT IDLETIME", "summary": "Get the time since a Redis object was last accessed", "arguments": "key", "since": "2.2.3"}, {"cmd": "OBJECT REFCOUNT", "summary": "Get the number of references to the value of the key", "arguments": "key", "since": "2.2.3"}, {"cmd": "PERSIST", "summary": "Remove the expiration from a key", "arguments": "key", "since": "2.2.0"}, {"cmd": "PEXPIRE", "summary": "Set a key's time to live in milliseconds", "arguments": "key milliseconds [condition]", "since": "2.6.0"}, {"cmd": "PEXPIREAT", "summary": "Set the expiration for a key as a UNIX timestamp specified in milliseconds", "arguments": "key milliseconds-timestamp [condition]", "since": "2.6.0"}, {"cmd": "PEXPIRETIME", "summary": "Get the expiration Unix timestamp for a key in milliseconds", "arguments": "key", "since": "7.0.0"}, {"cmd": "PFADD", "summary": "Adds the specified elements to the specified HyperLogLog.", "arguments": "key [element]", "since": "2.8.9"}, {"cmd": "PFCOUNT", "summary": "Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).", "arguments": "key", "since": "2.8.9"}, {"cmd": "PFDEBUG", "summary": "Internal commands for debugging HyperLogLog values", "arguments": "", "since": "2.8.9"}, {"cmd": "PFMERGE", "summary": "Merge N different HyperLogLogs into a single one.", "arguments": "destkey sourcekey", "since": "2.8.9"}, {"cmd": "PFSELFTEST", "summary": "An internal command for testing HyperLogLog values", "arguments": "", "since": "2.8.9"}, {"cmd": "PING", "summary": "Ping the server", "arguments": "[message]", "since": "1.0.0"}, {"cmd": "PSETEX", "summary": "Set the value and expiration in milliseconds of a key", "arguments": "key milliseconds value", "since": "2.6.0"}, {"cmd": "PSUBSCRIBE", "summary": "Listen for messages published to channels matching the given patterns", "arguments": "pattern", "since": "2.0.0"}, {"cmd": "PSYNC", "summary": "Internal command used for replication", "arguments": "replicationid offset", "since": "2.8.0"}, {"cmd": "PTTL", "summary": "Get the time to live for a key in milliseconds", "arguments": "key", "since": "2.6.0"}, {"cmd": "PUBLISH", "summary": "Post a message to a channel", "arguments": "channel message", "since": "2.0.0"}, {"cmd": "PUBSUB", "summary": "A container for Pub/Sun commands", "arguments": "", "since": "2.8.0"}, {"cmd": "PUBSUB CHANNELS", "summary": "List active channels", "arguments": "[pattern]", "since": "2.8.0"}, {"cmd": "PUBSUB HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "6.2.0"}, {"cmd": "PUBSUB NUMPAT", "summary": "Get the count of unique patterns pattern subscriptions", "arguments": "", "since": "2.8.0"}, {"cmd": "PUBSUB NUMSUB", "summary": "Get the count of subscribers for channels", "arguments": "[channel]", "since": "2.8.0"}, {"cmd": "PUNSUBSCRIBE", "summary": "Stop listening for messages posted to channels matching the given patterns", "arguments": "[pattern]", "since": "2.0.0"}, {"cmd": "QUIT", "summary": "Close the connection", "arguments": "", "since": "1.0.0"}, {"cmd": "RANDOMKEY", "summary": "Return a random key from the keyspace", "arguments": "", "since": "1.0.0"}, {"cmd": "READONLY", "summary": "Enables read queries for a connection to a cluster replica node", "arguments": "", "since": "3.0.0"}, {"cmd": "READWRITE", "summary": "Disables read queries for a connection to a cluster replica node", "arguments": "", "since": "3.0.0"}, {"cmd": "RENAME", "summary": "Rename a key", "arguments": "key newkey", "since": "1.0.0"}, {"cmd": "RENAMENX", "summary": "Rename a key, only if the new key does not exist", "arguments": "key newkey", "since": "1.0.0"}, {"cmd": "REPLCONF", "summary": "An internal command for configuring the replication stream", "arguments": "", "since": "3.0.0"}, {"cmd": "REPLICAOF", "summary": "Make the server a replica of another instance, or promote it as master.", "arguments": "host port", "since": "5.0.0"}, {"cmd": "RESET", "summary": "Reset the connection", "arguments": "", "since": "6.2.0"}, {"cmd": "RESTORE", "summary": "Create a key using the provided serialized value, previously obtained using DUMP.", "arguments": "key ttl serialized-value [replace] [absttl] [seconds] [frequency]", "since": "2.6.0"}, {"cmd": "RESTORE-ASKING", "summary": "An internal command for migrating keys in a cluster", "arguments": "", "since": "3.0.0"}, {"cmd": "ROLE", "summary": "Return the role of the instance in the context of replication", "arguments": "", "since": "2.8.12"}, {"cmd": "RPOP", "summary": "Remove and get the last elements in a list", "arguments": "key [count]", "since": "1.0.0"}, {"cmd": "RPOPLPUSH", "summary": "Remove the last element in a list, prepend it to another list and return it", "arguments": "source destination", "since": "1.2.0"}, {"cmd": "RPUSH", "summary": "Append one or multiple elements to a list", "arguments": "key element", "since": "1.0.0"}, {"cmd": "RPUSHX", "summary": "Append an element to a list, only if the list exists", "arguments": "key element", "since": "2.2.0"}, {"cmd": "SADD", "summary": "Add one or more members to a set", "arguments": "key member", "since": "1.0.0"}, {"cmd": "SAVE", "summary": "Synchronously save the dataset to disk", "arguments": "", "since": "1.0.0"}, {"cmd": "SCAN", "summary": "Incrementally iterate the keys space", "arguments": "cursor [pattern] [count] [type]", "since": "2.8.0"}, {"cmd": "SCARD", "summary": "Get the number of members in a set", "arguments": "key", "since": "1.0.0"}, {"cmd": "SCRIPT", "summary": "A container for Lua scripts management commands", "arguments": "", "since": "2.6.0"}, {"cmd": "SCRIPT DEBUG", "summary": "Set the debug mode for executed scripts.", "arguments": "mode", "since": "3.2.0"}, {"cmd": "SCRIPT EXISTS", "summary": "Check existence of scripts in the script cache.", "arguments": "sha1", "since": "2.6.0"}, {"cmd": "SCRIPT FLUSH", "summary": "Remove all the scripts from the script cache.", "arguments": "[async]", "since": "2.6.0"}, {"cmd": "SCRIPT HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "5.0.0"}, {"cmd": "SCRIPT KILL", "summary": "Kill the script currently in execution.", "arguments": "", "since": "2.6.0"}, {"cmd": "SCRIPT LOAD", "summary": "Load the specified Lua script into the script cache.", "arguments": "script", "since": "2.6.0"}, {"cmd": "SDIFF", "summary": "Subtract multiple sets", "arguments": "key", "since": "1.0.0"}, {"cmd": "SDIFFSTORE", "summary": "Subtract multiple sets and store the resulting set in a key", "arguments": "destination key", "since": "1.0.0"}, {"cmd": "SELECT", "summary": "Change the selected database for the current connection", "arguments": "index", "since": "1.0.0"}, {"cmd": "SET", "summary": "Set the string value of a key", "arguments": "key value [expiration] [condition] [get]", "since": "1.0.0"}, {"cmd": "SETBIT", "summary": "Sets or clears the bit at offset in the string value stored at key", "arguments": "key offset value", "since": "2.2.0"}, {"cmd": "SETEX", "summary": "Set the value and expiration of a key", "arguments": "key seconds value", "since": "2.0.0"}, {"cmd": "SETNX", "summary": "Set the value of a key, only if the key does not exist", "arguments": "key value", "since": "1.0.0"}, {"cmd": "SETRANGE", "summary": "Overwrite part of a string at key starting at the specified offset", "arguments": "key offset value", "since": "2.2.0"}, {"cmd": "SHUTDOWN", "summary": "Synchronously save the dataset to disk and then shut down the server", "arguments": "[nosave_save] [now] [force] [abort]", "since": "1.0.0"}, {"cmd": "SINTER", "summary": "Intersect multiple sets", "arguments": "key", "since": "1.0.0"}, {"cmd": "SINTERCARD", "summary": "Intersect multiple sets and return the cardinality of the result", "arguments": "numkeys key [limit]", "since": "7.0.0"}, {"cmd": "SINTERSTORE", "summary": "Intersect multiple sets and store the resulting set in a key", "arguments": "destination key", "since": "1.0.0"}, {"cmd": "SISMEMBER", "summary": "Determine if a given value is a member of a set", "arguments": "key member", "since": "1.0.0"}, {"cmd": "SLAVEOF", "summary": "Make the server a replica of another instance, or promote it as master. Deprecated starting with Redis 5. Use REPLICAOF instead.", "arguments": "host port", "since": "1.0.0"}, {"cmd": "SLOWLOG", "summary": "A container for slow log commands", "arguments": "", "since": "2.2.12"}, {"cmd": "SLOWLOG GET", "summary": "Get the slow log's entries", "arguments": "[count]", "since": "2.2.12"}, {"cmd": "SLOWLOG HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "6.2.0"}, {"cmd": "SLOWLOG LEN", "summary": "Get the slow log's length", "arguments": "", "since": "2.2.12"}, {"cmd": "SLOWLOG RESET", "summary": "Clear all entries from the slow log", "arguments": "", "since": "2.2.12"}, {"cmd": "SMEMBERS", "summary": "Get all the members in a set", "arguments": "key", "since": "1.0.0"}, {"cmd": "SMISMEMBER", "summary": "Returns the membership associated with the given elements for a set", "arguments": "key member", "since": "6.2.0"}, {"cmd": "SMOVE", "summary": "Move a member from one set to another", "arguments": "source destination member", "since": "1.0.0"}, {"cmd": "SORT", "summary": "Sort the elements in a list, set or sorted set", "arguments": "key [pattern] [offset_count] [pattern] [order] [sorting] [destination]", "since": "1.0.0"}, {"cmd": "SORT_RO", "summary": "Sort the elements in a list, set or sorted set. Read-only variant of SORT.", "arguments": "key [pattern] [offset_count] [pattern] [order] [sorting]", "since": "7.0.0"}, {"cmd": "SPOP", "summary": "Remove and return one or multiple random members from a set", "arguments": "key [count]", "since": "1.0.0"}, {"cmd": "SRANDMEMBER", "summary": "Get one or multiple random members from a set", "arguments": "key [count]", "since": "1.0.0"}, {"cmd": "SREM", "summary": "Remove one or more members from a set", "arguments": "key member", "since": "1.0.0"}, {"cmd": "SSCAN", "summary": "Incrementally iterate Set elements", "arguments": "key cursor [pattern] [count]", "since": "2.8.0"}, {"cmd": "STRLEN", "summary": "Get the length of the value stored in a key", "arguments": "key", "since": "2.2.0"}, {"cmd": "SUBSCRIBE", "summary": "Listen for messages published to the given channels", "arguments": "channel", "since": "2.0.0"}, {"cmd": "SUBSTR", "summary": "Get a substring of the string stored at a key", "arguments": "key start end", "since": "1.0.0"}, {"cmd": "SUNION", "summary": "Add multiple sets", "arguments": "key", "since": "1.0.0"}, {"cmd": "SUNIONSTORE", "summary": "Add multiple sets and store the resulting set in a key", "arguments": "destination key", "since": "1.0.0"}, {"cmd": "SWAPDB", "summary": "Swaps two Redis databases", "arguments": "index1 index2", "since": "4.0.0"}, {"cmd": "SYNC", "summary": "Internal command used for replication", "arguments": "", "since": "1.0.0"}, {"cmd": "TIME", "summary": "Return the current server time", "arguments": "", "since": "2.6.0"}, {"cmd": "TOUCH", "summary": "Alters the last access time of a key(s). Returns the number of existing keys specified.", "arguments": "key", "since": "3.2.1"}, {"cmd": "TTL", "summary": "Get the time to live for a key in seconds", "arguments": "key", "since": "1.0.0"}, {"cmd": "TYPE", "summary": "Determine the type stored at key", "arguments": "key", "since": "1.0.0"}, {"cmd": "UNLINK", "summary": "Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.", "arguments": "key", "since": "4.0.0"}, {"cmd": "UNSUBSCRIBE", "summary": "Stop listening for messages posted to the given channels", "arguments": "[channel]", "since": "2.0.0"}, {"cmd": "UNWATCH", "summary": "Forget about all watched keys", "arguments": "", "since": "2.2.0"}, {"cmd": "WAIT", "summary": "Wait for the synchronous replication of all the write commands sent in the context of the current connection", "arguments": "numreplicas timeout", "since": "3.0.0"}, {"cmd": "WATCH", "summary": "Watch the given keys to determine execution of the MULTI/EXEC block", "arguments": "key", "since": "2.2.0"}, {"cmd": "XACK", "summary": "Marks a pending message as correctly processed, effectively removing it from the pending entries list of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, the IDs we were actually able to resolve in the PEL.", "arguments": "key group id", "since": "5.0.0"}, {"cmd": "XADD", "summary": "Appends a new entry to a stream", "arguments": "key [nomkstream] [trim] id_or_auto field_value", "since": "5.0.0"}, {"cmd": "XAUTOCLAIM", "summary": "Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to the specified consumer.", "arguments": "key group consumer min-idle-time start [count] [justid]", "since": "6.2.0"}, {"cmd": "XCLAIM", "summary": "Changes (or acquires) ownership of a message in a consumer group, as if the message was delivered to the specified consumer.", "arguments": "key group consumer min-idle-time id [ms] [ms-unix-time] [count] [force] [justid]", "since": "5.0.0"}, {"cmd": "XDEL", "summary": "Removes the specified entries from the stream. Returns the number of items actually deleted, that may be different from the number of IDs passed in case certain IDs do not exist.", "arguments": "key id", "since": "5.0.0"}, {"cmd": "XGROUP", "summary": "A container for consumer groups commands", "arguments": "", "since": "5.0.0"}, {"cmd": "XGROUP CREATE", "summary": "Create a consumer group.", "arguments": "key groupname id [mkstream]", "since": "5.0.0"}, {"cmd": "XGROUP CREATECONSUMER", "summary": "Create a consumer in a consumer group.", "arguments": "key groupname consumername", "since": "6.2.0"}, {"cmd": "XGROUP DELCONSUMER", "summary": "Delete a consumer from a consumer group.", "arguments": "key groupname consumername", "since": "5.0.0"}, {"cmd": "XGROUP DESTROY", "summary": "Destroy a consumer group.", "arguments": "key groupname", "since": "5.0.0"}, {"cmd": "XGROUP HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "5.0.0"}, {"cmd": "XGROUP SETID", "summary": "Set a consumer group to an arbitrary last delivered ID value.", "arguments": "key groupname id", "since": "5.0.0"}, {"cmd": "XINFO", "summary": "A container for stream introspection commands", "arguments": "", "since": "5.0.0"}, {"cmd": "XINFO CONSUMERS", "summary": "List the consumers in a consumer group", "arguments": "key groupname", "since": "5.0.0"}, {"cmd": "XINFO GROUPS", "summary": "List the consumer groups of a stream", "arguments": "key", "since": "5.0.0"}, {"cmd": "XINFO HELP", "summary": "Show helpful text about the different subcommands", "arguments": "", "since": "5.0.0"}, {"cmd": "XINFO STREAM", "summary": "Get information about a stream", "arguments": "key [full]", "since": "5.0.0"}, {"cmd": "XLEN", "summary": "Return the number of entries in a stream", "arguments": "key", "since": "5.0.0"}, {"cmd": "XPENDING", "summary": "Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged.", "arguments": "key group [filters]", "since": "5.0.0"}, {"cmd": "XRANGE", "summary": "Return a range of elements in a stream, with IDs matching the specified IDs interval", "arguments": "key start end [count]", "since": "5.0.0"}, {"cmd": "XREAD", "summary": "Return never seen elements in multiple streams, with IDs greater than the ones reported by the caller for each stream. Can block.", "arguments": "[count] [milliseconds] streams", "since": "5.0.0"}, {"cmd": "XREADGROUP", "summary": "Return new entries from a stream using a consumer group, or access the history of the pending entries for a given consumer. Can block.", "arguments": "group_consumer [count] [milliseconds] [noack] streams", "since": "5.0.0"}, {"cmd": "XREVRANGE", "summary": "Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE", "arguments": "key end start [count]", "since": "5.0.0"}, {"cmd": "XSETID", "summary": "An internal command for replicating stream values", "arguments": "key last-id", "since": "5.0.0"}, {"cmd": "XTRIM", "summary": "Trims the stream to (approximately if '~' is passed) a certain size", "arguments": "key trim", "since": "5.0.0"}, {"cmd": "ZADD", "summary": "Add one or more members to a sorted set, or update its score if it already exists", "arguments": "key [condition] [comparison] [change] [increment] score_member", "since": "1.2.0"}, {"cmd": "ZCARD", "summary": "Get the number of members in a sorted set", "arguments": "key", "since": "1.2.0"}, {"cmd": "ZCOUNT", "summary": "Count the members in a sorted set with scores within the given values", "arguments": "key min max", "since": "2.0.0"}, {"cmd": "ZDIFF", "summary": "Subtract multiple sorted sets", "arguments": "numkeys key [withscores]", "since": "6.2.0"}, {"cmd": "ZDIFFSTORE", "summary": "Subtract multiple sorted sets and store the resulting sorted set in a new key", "arguments": "destination numkeys key", "since": "6.2.0"}, {"cmd": "ZINCRBY", "summary": "Increment the score of a member in a sorted set", "arguments": "key increment member", "since": "1.2.0"}, {"cmd": "ZINTER", "summary": "Intersect multiple sorted sets", "arguments": "numkeys key [weight] [aggregate] [withscores]", "since": "6.2.0"}, {"cmd": "ZINTERCARD", "summary": "Intersect multiple sorted sets and return the cardinality of the result", "arguments": "numkeys key [limit]", "since": "7.0.0"}, {"cmd": "ZINTERSTORE", "summary": "Intersect multiple sorted sets and store the resulting sorted set in a new key", "arguments": "destination numkeys key [weight] [aggregate]", "since": "2.0.0"}, {"cmd": "ZLEXCOUNT", "summary": "Count the number of members in a sorted set between a given lexicographical range", "arguments": "key min max", "since": "2.8.9"}, {"cmd": "ZMPOP", "summary": "Remove and return members with scores in a sorted set", "arguments": "numkeys key where [count]", "since": "7.0.0"}, {"cmd": "ZMSCORE", "summary": "Get the score associated with the given members in a sorted set", "arguments": "key member", "since": "6.2.0"}, {"cmd": "ZPOPMAX", "summary": "Remove and return members with the highest scores in a sorted set", "arguments": "key [count]", "since": "5.0.0"}, {"cmd": "ZPOPMIN", "summary": "Remove and return members with the lowest scores in a sorted set", "arguments": "key [count]", "since": "5.0.0"}, {"cmd": "ZRANDMEMBER", "summary": "Get one or multiple random elements from a sorted set", "arguments": "key [options]", "since": "6.2.0"}, {"cmd": "ZRANGE", "summary": "Return a range of members in a sorted set", "arguments": "key min max [sortby] [rev] [offset_count] [withscores]", "since": "1.2.0"}, {"cmd": "ZRANGEBYLEX", "summary": "Return a range of members in a sorted set, by lexicographical range", "arguments": "key min max [offset_count]", "since": "2.8.9"}, {"cmd": "ZRANGEBYSCORE", "summary": "Return a range of members in a sorted set, by score", "arguments": "key min max [withscores] [offset_count]", "since": "1.0.5"}, {"cmd": "ZRANGESTORE", "summary": "Store a range of members from sorted set into another key", "arguments": "dst src min max [sortby] [rev] [offset_count]", "since": "6.2.0"}, {"cmd": "ZRANK", "summary": "Determine the index of a member in a sorted set", "arguments": "key member", "since": "2.0.0"}, {"cmd": "ZREM", "summary": "Remove one or more members from a sorted set", "arguments": "key member", "since": "1.2.0"}, {"cmd": "ZREMRANGEBYLEX", "summary": "Remove all members in a sorted set between the given lexicographical range", "arguments": "key min max", "since": "2.8.9"}, {"cmd": "ZREMRANGEBYRANK", "summary": "Remove all members in a sorted set within the given indexes", "arguments": "key start stop", "since": "2.0.0"}, {"cmd": "ZREMRANGEBYSCORE", "summary": "Remove all members in a sorted set within the given scores", "arguments": "key min max", "since": "1.2.0"}, {"cmd": "ZREVRANGE", "summary": "Return a range of members in a sorted set, by index, with scores ordered from high to low", "arguments": "key start stop [withscores]", "since": "1.2.0"}, {"cmd": "ZREVRANGEBYLEX", "summary": "Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.", "arguments": "key max min [offset_count]", "since": "2.8.9"}, {"cmd": "ZREVRANGEBYSCORE", "summary": "Return a range of members in a sorted set, by score, with scores ordered from high to low", "arguments": "key max min [withscores] [offset_count]", "since": "2.2.0"}, {"cmd": "ZREVRANK", "summary": "Determine the index of a member in a sorted set, with scores ordered from high to low", "arguments": "key member", "since": "2.0.0"}, {"cmd": "ZSCAN", "summary": "Incrementally iterate sorted sets elements and associated scores", "arguments": "key cursor [pattern] [count]", "since": "2.8.0"}, {"cmd": "ZSCORE", "summary": "Get the score associated with the given member in a sorted set", "arguments": "key member", "since": "1.2.0"}, {"cmd": "ZUNION", "summary": "Add multiple sorted sets", "arguments": "numkeys key [weight] [aggregate] [withscores]", "since": "6.2.0"}, {"cmd": "ZUNIONSTORE", "summary": "Add multiple sorted sets and store the resulting sorted set in a new key", "arguments": "destination numkeys key [weight] [aggregate]", "since": "2.0.0"}] ================================================ FILE: src/resources/commands.qrc ================================================ commands.json ================================================ FILE: src/resources/convert_commands.py ================================================ import json def convert_redis_io_commands(path): with open(path) as f: commands = json.load(f) converted = [] for cmd, info in commands.items(): arguments = [] for arg in info.get('arguments', []): parts = [] if 'command' in arg: parts.append(arg["command"]) if type == "enum": parts.append("|".join(arg['enum'])) elif "name" in arg: if isinstance(arg['name'], list): parts.append(" ".join(arg['name'])) else: parts.append(arg['name']) arg_spec = " ".join(parts) if arg.get('optional', False): arguments.append("[%s]" % arg_spec) else: arguments.append(arg_spec) converted.append({ "cmd": cmd, "summary": info["summary"], "arguments": " ".join(arguments), "since": info["since"] }) with open("%s.converted" % path, "w") as fres: json.dump(converted, fres) def detect_key_positions(path): with open(path) as f: commands = json.load(f) for cmd, info in commands.items(): for index, arg in enumerate(info.get('arguments', [])): if 'type' in arg and arg['type'] == 'key': print('{"%s", %s},' % (cmd, index)) if __name__ == "__main__": convert_redis_io_commands("commands.json") #detect_key_positions("commands_raw.json") ================================================ FILE: src/resources/flatpak/app.resp.RESP.desktop ================================================ [Desktop Entry] Version=1.0 Name=RESP.app Comment=Cross-platform open source database management tool for Redis ® Type=Application Categories=Development; Exec=resp Terminal=false StartupNotify=true Keywords=RDM;Redis; Icon=app.resp.RESP ================================================ FILE: src/resources/flatpak/app.resp.RESP.metainfo.xml ================================================ app.resp.RESP CC0-1.0 GPL-3.0-only RESP.app - GUI for Redis ® Cross-platform open source database management tool for Redis ®

RESP (formerly RedisDesktopManager) — is a fast open source Redis ® database management application for Windows, Linux and MacOS. This tool offers you an easy-to-use GUI to access your Redis ® DB and perform some basic operations: view keys as a tree, CRUD keys, execute commands via shell. RESP supports SSL/TLS encryption, SSH tunnels and cloud Redis instances, such as: Amazon ElastiCache, Microsoft Azure Redis Cache and other Redis ® clouds.

https://resp.app/ app.resp.RESP.desktop https://resp.app/static/img/features/all.png?v=20202
================================================ FILE: src/resources/fonts.qrc ================================================ fonts/OpenSans.ttc fonts/Inconsolata-Regular.ttf ================================================ FILE: src/resources/icons.qrc ================================================ images/dark_theme/server_group.svg images/dark_theme/search.svg images/dark_theme/pub-sub-channels.svg images/dark_theme/list.svg images/dark_theme/code_file.svg images/dark_theme/offline.svg images/dark_theme/binary_file.svg images/dark_theme/alert.svg images/dark_theme/live_update.svg images/dark_theme/bulk_operations.svg images/dark_theme/cleanup.svg images/dark_theme/github.svg images/dark_theme/export.svg images/dark_theme/database.svg images/dark_theme/file.svg images/dark_theme/clear.svg images/dark_theme/maximize.svg images/dark_theme/twi.svg images/dark_theme/slowlog.svg images/dark_theme/server_group_open.svg images/dark_theme/key.svg images/dark_theme/minimize.svg images/dark_theme/settings.svg images/dark_theme/db-copy.svg images/dark_theme/import.svg images/dark_theme/server.svg images/dark_theme/ok.svg images/dark_theme/copy_2.svg images/dark_theme/telegram.svg images/dark_theme/square-half.svg images/dark_theme/cluster.svg images/dark_theme/server-config.svg images/dark_theme/plus.svg images/dark_theme/document.svg images/dark_theme/namespace_open.svg images/dark_theme/loader.svg images/dark_theme/add.svg images/dark_theme/server-stats.svg images/dark_theme/copy.svg images/dark_theme/refresh.svg images/dark_theme/clients.svg images/dark_theme/wait.svg images/dark_theme/save.svg images/dark_theme/back.svg images/dark_theme/live_update_disable.svg images/dark_theme/sentinel.svg images/dark_theme/log.svg images/dark_theme/delete.svg images/dark_theme/server_offline.svg images/dark_theme/cleanup_filtered.svg images/dark_theme/sort.svg images/dark_theme/namespace.svg images/dark_theme/filter.svg images/dark_theme/console.svg images/dark_theme/help.svg images/dark_theme/server_2.svg images/dark_theme/memory_usage.svg images/dark_theme/ttl.svg images/light_theme/server_group.svg images/light_theme/search.svg images/light_theme/pub-sub-channels.svg images/light_theme/list.svg images/light_theme/code_file.svg images/light_theme/offline.svg images/light_theme/binary_file.svg images/light_theme/alert.svg images/light_theme/live_update.svg images/light_theme/bulk_operations.svg images/light_theme/cleanup.svg images/light_theme/github.svg images/light_theme/export.svg images/light_theme/database.svg images/light_theme/file.svg images/light_theme/clear.svg images/light_theme/maximize.svg images/light_theme/twi.svg images/light_theme/slowlog.svg images/light_theme/server_group_open.svg images/light_theme/key.svg images/light_theme/minimize.svg images/light_theme/settings.svg images/light_theme/db-copy.svg images/light_theme/import.svg images/light_theme/server.svg images/light_theme/ok.svg images/light_theme/copy_2.svg images/light_theme/telegram.svg images/light_theme/square-half.svg images/light_theme/cluster.svg images/light_theme/server-config.svg images/light_theme/plus.svg images/light_theme/document.svg images/light_theme/namespace_open.svg images/light_theme/loader.svg images/light_theme/add.svg images/light_theme/server-stats.svg images/light_theme/copy.svg images/light_theme/refresh.svg images/light_theme/clients.svg images/light_theme/wait.svg images/light_theme/save.svg images/light_theme/back.svg images/light_theme/live_update_disable.svg images/light_theme/sentinel.svg images/light_theme/log.svg images/light_theme/delete.svg images/light_theme/server_offline.svg images/light_theme/cleanup_filtered.svg images/light_theme/sort.svg images/light_theme/namespace.svg images/light_theme/filter.svg images/light_theme/console.svg images/light_theme/help.svg images/light_theme/server_2.svg images/light_theme/memory_usage.svg images/light_theme/ttl.svg ================================================ FILE: src/resources/icons_qrc_generator.py ================================================ import glob import os DARK_THEME_PATH = "images/dark_theme" LIGHT_THEME_PATH = "images/light_theme" def generate_qrc(): with open("icons.qrc", "w") as output: dark_lines = [] light_lines = [] for icon_file in glob.glob("./%s/*.svg" % DARK_THEME_PATH): base_name = os.path.basename(icon_file) if not os.path.exists("./%s/%s" % (LIGHT_THEME_PATH, base_name)): print("Icon %s doesn't exist in light theme" % base_name) return dark_lines.append(f"{DARK_THEME_PATH}/{base_name}") light_lines.append(f"{LIGHT_THEME_PATH}/{base_name}") output.write("\n") output.write(f"\n ") output.write("\n ".join(dark_lines)) output.write("\n\n") output.write(f"\n ") output.write("\n ".join(light_lines)) output.write("\n\n") output.write("") if __name__ == "__main__": generate_qrc() ================================================ FILE: src/resources/images.qrc ================================================ images/logo.png logo.icns images/digitalocean_logo.svg images/aws_logo.svg images/aws_logo_white.svg images/azure_logo.svg images/heroku_logo.svg images/redisinsight.svg ================================================ FILE: src/resources/resp.desktop ================================================ [Desktop Entry] Version=1.0 Name=RESP.app Comment=Developer GUI for Redis Type=Application Categories=Development; Exec=/opt/resp_app/resp Terminal=false StartupNotify=true Icon=resp Keywords=RDM;Redis; ================================================ FILE: src/resources/tr.qrc ================================================ translations/rdm_zh_CN.qm translations/rdm_zh_TW.qm translations/rdm_es_ES.qm translations/rdm_ja_JP.qm translations/rdm_uk_UA.qm ================================================ FILE: src/resources/translations/rdm.ts ================================================ QObject Cannot connect to cluster node %1:%2 Cannot flush db (%1): %2 RESP Settings directory is not writable RESP.app can't save connections file to settings directory. Please change file permissions or restart RESP.app as administrator. Cannot parse scan response Server returned unexpected response: Cannot set TTL for key %1 Cannot rename key %1: %2 Cannot persist key '%1'. <br> Key does not exist or does not have an assigned TTL value Cannot load rows for key %1: %2 Invalid row Value with the same key already exists Connection error: Data was loaded from server partially. Cannot load key %1, connection error occurred: %2 Cannot load key %1 because it doesn't exist in database. Please reload connection tree and try again. Cannot retrieve type of the key: Cannot open file with key value Cannot connect to server '%1'. Check log for details. Open Source version of RESP.app <b>doesn't support SSH tunneling</b>.<br /><br /> To get fully-featured application, please buy subscription on <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Every single subscription gives us funds to continue the development process and provide support to our users. <br />If you have any questions please feel free to contact us at <a href='mailto:support@resp.app'>support@resp.app</a> or join <a href='https://t.me/RedisDesktopManager'>Telegram chat</a>. Cannot load keys: %1 Delete key error: %1 Cannot determine amount of used memory by key: %1 Cannot flush database: Invalid Connection. Check connection settings. Live update was disabled due to exceeded keys limit. Please specify filter more carefully or change limit in settings. Key was added. Do you want to reload keys in selected database? Key was added Do you really want to remove all keys from this database? Cannot load databases: Live update was disabled Rename key New name: Total pages: Size: TTL: Set key TTL New TTL: Delete Delete key Changes are not saved Do you want to close key tab without saving changes? Persist key Do you really want to delete this key? Reload Value Add Row Add Element to HLL Add Delete row The row is the last one in the key. After removing it key will be deleted. Do you really want to remove this row? Search on page... Full Search Value and Console tabs related to this connection will be closed. Do you want to continue? Do you really want to delete connection? Connected to cluster. Connected. Switch to %1 mode. Close console tab to stop listen for messages. Subscribe error: %1 Server %0 Can't find formatter: %1 Invalid callback Can't load list of available formatters from extension server: %1 Can't encode value: %1 Loading key: %1 from db %2 Cannot open value tab Connection error Connection error. Can't open value tab. Cannot reload key value: %1 Cannot load key value: %1 Connect to Redis Server Import Import Connections Export Connections Report issue Documentation Join Telegram Chat Follow Star on GitHub! Log Extension Server Settings New Connection Settings How to connect Connection Settings Create connection from Redis URL Learn more about Redis URL: Connection guides Local or Public Redis Redis with SSL/TLS SSH tunnel UNIX socket Cannot figure out how to connect to your redis-server? <a href="https://docs.resp.app/en/latest/quick-start/">Read the Docs</a>, <a href="mailto:support@resp.app">Contact Support</a> or ask for help in our <a href="https://t.me/RedisDesktopManager">Telegram Group</a> Don't have running Redis? Spin up hassle-free Redis on Digital Ocean Skip Name: Connection Name Address: redis-server host For better network performance please use 127.0.0.1 (Optional) redis-server authentication password Username: (Optional) redis-server authentication username (Redis >6.0) Security Public Key: (Optional) Public Key in PEM format Select public key in PEM format (Optional) Private Key in PEM format Select private key in PEM format Authority: (Optional) Authority in PEM format Select authority file in PEM format SSH Tunnel SSH Address: Remote Host with SSH server SSH User: Valid SSH User Name Private Key Path to Private Key in PEM format <b>Tip:</b> Use <code>⌘ + Shift + .</code> to show hidden files and folders in dialog Password SSH User Password Enable TLS-over-SSH (<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) Advanced Settings Sign in with RESP.app account Renew your subscription You have no active subscription No internet connection Your trial has ended To use this version you need to renew your subscription. Please make sure that RESP.app is not blocked by a firewall and you have an internet connection. If you’re behind a proxy please enable option before sign-in. Please purchase a subscription to continue using RESP.app. If you have any questions please contact support Renew Subscription Buy Subscription Try Again Email: Password: Forgot password? Application will be restarted to apply this setting. Sign In Please enter email & password to sign in. Offline Activation Paste Activation code here Where can I find my activation code? Activate Please enter valid activation code. SSL / TLS Enable strict mode: Use SSH Agent (Optional) Custom SSH Agent Path Select SSH Agent Additional configuration is required to enable SSH Agent support Passphrase for provided private key Password request will be prompt prior to connection Ask for password Keys loading Default filter: Pattern which defines loaded keys from redis-server Namespace Separator: Separator used for namespace extraction from keys Timeouts & Limits Connection Timeout (sec): Execution Timeout (sec): Databases discovery limit: Cluster Change host on cluster redirects: Formatters Default value formatter: Auto detect (JSON / Plain Text / HEX) Last selected Select formatter ... Appearance Icon color: Invalid settings detected! Test Connection OK Cancel General Application will be restarted to apply these settings. Language Font Font Size Dark Mode Maximum Formatted Value Size Size in bytes Maximum amount of items per page Show only last part for namespaced keys Use system proxy settings Use system proxy only for HTTP(S) requests Value Editor Connections Tree Show namespaced keys on top Reopen namespaces on reload (Disable to improve treeview performance) Limit for SCAN command Maximum amount of rendered child items Live update maximum allowed keys Live update interval (in seconds) Server Url: Basic Auth: User Response timeout (in seconds) Available Data Formatters Reload Id Name Read Only Version Quick Start Guide Successful connection to redis-server Can't connect to redis-server Add Group Regroup connections Exit Regroup Mode Show password (Removed) Open Keys Filter Reload Keys in Database Add New Key Disable Live Update Enable Live Update Open Console Analyze Used Memory Bulk Operations Flush Database Delete keys with filter Set TTL for multiple keys Copy keys from this database to another Import keys from RDB file Back Copy Key Name Reload Namespace Copy Namespace Pattern Delete Namespace Disconnect Reload Server Unload All Data Edit Connection Settings Duplicate Connection Delete Connection Connecting... Clear Arguments Description Available since Close View Server Info Redis Version Used memory Cmd Processed Monitor Commands Clients Server Actions Uptime Hit Ratio Server Stats Console day(s) Commands Per Second Ops/s Connected Clients Memory Usage Mb Network Input Kb/s Network Output Total Error Replies Error Replies Auto Refresh Property Value Subscribe in Console Slowlog Pub/Sub Channels Enable Channel Name Command Processed at Execution Time (μs) Client Address Age (sec) Idle Flags Current Database Add New Key to Key: Type: Or Import Value from the file (Optional) Any file Select file with value Save Edit Connections Group Add New Connections Group Group Name: Error Page Enter valid value Formatting error Unknown formatter error (Empty response) [Binary] Copy to Clipboard Exit Full Screen Mode Save Changes Search string Find Next Find Regex Cannot find more results Try to decompress: Decompressed: Cannot decompress value using Cannot find any results Binary value is too large to display View as: Large value (>150kB). Formatters are not available. Score Bulk Operations Manager Invalid RDB path Please specify valid path to RDB file Delete keys Set TTL Copy keys to another database Copy keys Import data from rdb file Redis Server: Database number: Path to RDB file: Path to dump.rdb file Select dump.rdb Select DB in RDB file: Import keys that match <b>regex</b>: Key pattern: Destination Redis Server: Destination Redis Server Database Index: Show matched keys Show Affected keys Matched keys: Affected keys: Bulk Operation finished. Bulk Operation finished with errors Processed: Getting list of affected keys... Success Confirmation Do you really want to perform bulk operation? ID Value (represented as JSON object) The row has been changed on server.Reload and try again. Failed to perform actions on %1 keys. Cannot copy key Source connection error Target connection error Cannot remove key Cannot execute command Invalid regexp for keys filter. Cannot get the list of affected keys Cannot set TTL for key Your redis-server doesn't support <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a> commands. Key was added. Do you want to reload keys in selected namespace? Select File Save value to file Save Raw Value to File Save Formatted Value to File Save Raw Value Save Formatted Value Value was saved to file: Cannot connect to redis-server Edit Connection Group Delete Connection Group Do you really want to delete group <b>with all connections</b>? Order of elements: Default Reverse Start date should be less than End date Apply filter <span style="font-size: 11px;">Powered by awesome <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">open-source software</a> and <a href="http://icons8.com/">icons8</a>.</span> Getting Started Thank you for choosing RESP.app. Let's make your Redis experience better. Connect to Redis-Server Read the Docs Load more keys SSH Passphrase Unknown Passphrase Continue Yes No Trial is active till Licensed to Subscription is active until: Manage Subscription Network is not accessible. Please ensure that you have internet access and try again. Invalid login or password Too many requests from your IP Unknown error. Status code %1 Cannot parse server reply Cannot validate token Cannot login - %1. <br/> Please try again or contact <a href='mailto:support@resp.app'>support@resp.app</a> Cannot save the update. Disk is full or download folder is not writable. Download was canceled Network error Expired activation code Invalid activation code Select Unsupported Redis Data type Cannot delete key: ================================================ FILE: src/resources/translations/rdm_es_ES.ts ================================================ QObject Cannot connect to cluster node %1:%2 No se puede conectar al nodo del cluster %1:%2 Cannot flush db (%1): %2 No se puede vaciar db (%1): %2 RESP Settings directory is not writable El directorio de ajustes no es grabable RESP.app can't save connections file to settings directory. Please change file permissions or restart RESP.app as administrator. RESP.app no puede guardar las conexiones en el directorio de configuraciones. Cambia los permisos o reinicia RESP.app como administrador. RDM can't save connections file to settings directory. Please change file permissions or restart RDM as administrator. RDM no puede grabar el fichero de conexiones en el directorio de ajustes. Por favor cambia los permisos del fichero o reinicia RDM como administrador. Cannot rename key %1: %2 No se puede renombrar la clave %1: %2 Cannot persist key '%1'. <br> Key does not exist or does not have an assigned TTL value La clave '%1' no se pudo persistir. <br> La llave no parece existir o tener un tiempo de espera asociado Cannot parse scan response No se puede interpretar la respuesta Server returned unexpected response: El servidor ha devuelto una respuesta inesperada: Cannot set TTL for key %1 No se puede asignar el TTL para la clave %1 Cannot load rows for key %1: %2 No se pueden cargar las filas para la clave %1: %2 Invalid row Fila inválida Value with the same key already exists Ya existe un valor con la misma clave Connection error: Error de conexión: Data was loaded from server partially. Los datos se cargaron parcialmente desde el servidor. Cannot load key %1, connection error occurred: %2 No se puede cargar la clave %1, error de conexión: %2 Cannot load key %1 because it doesn't exist in database. Please reload connection tree and try again. No se puede cargar la clave %1 porque no existe en la base de datos. Por favor recarga el árbol de conexión e inténtalo de nuevo. Cannot load TTL for key %1, connection error occurred: %2 No se puede cargar el TTL para la clave %1, error de conexión: %2 Cannot retrieve type of the key: No se puede recuperar el tipo de la clave: Cannot open file with key value No se puede abrir el fichero con clave valor Unsupported Redis Data type %1 Tipo de datos Redis no soportado %1 Cannot connect to server '%1'. Check log for details. No se puede conectar al servidor %1. Compruba el log para más detalles. Open Source version of RDM <b>doesn't support SSH tunneling</b>.<br /><br /> To get fully-featured application, please buy subscription on <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Every single subscription gives us funds to continue the development process and provide support to our users. <br />If you have any questions please feel free to contact us at <a href='mailto:support@resp.app'>support@resp.app</a> or join <a href='https://t.me/RedisDesktopManager'>Telegram chat</a>. La versión Open Source de RDM <b>no soporta túneles SSH</b>.<br /><br /> Para obtener todas las características, por favor, compra un suscripción en <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Cada suscripción individual nos proporciona fondos para continuar el proceso de desarrollo y proporcionar soporte a nuestros usuarios. <br />Si tienes alguna pregunta, por favor, contacta con nosotros en <a href='mailto:support@resp.app'>support@resp.app</a> or join <a href='https://t.me/RedisDesktopManager'>Chat Telegram</a>. Open Source version of RESP.app <b>doesn't support SSH tunneling</b>.<br /><br /> To get fully-featured application, please buy subscription on <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Every single subscription gives us funds to continue the development process and provide support to our users. <br />If you have any questions please feel free to contact us at <a href='mailto:support@resp.app'>support@resp.app</a> or join <a href='https://t.me/RedisDesktopManager'>Telegram chat</a>. Cannot load keys: %1 No se pueden cargar las claves: %1 Delete key error: %1 Error: %1 borrando clave Cannot determine amount of used memory by key: %1 No se puede determinar la memoria usada por la clave: %1 Cannot flush database: No se puede vaciar la base de datos: Invalid Connection. Check connection settings. Conexión inválida. Comprueba ajustes de conexión. Live update was disabled due to exceeded keys limit. Please specify filter more carefully or change limit in settings. Actualización automática se ha desactivado al superarse el límite de claves. Por favor especifica un filtro más restrictivo o cambia el límite en ajustes. Key was added. Do you want to reload keys in selected database? Clave añadida. ¿Quieres recargar las claves en la base de datos seleccionada? Key was added Clave añadida Another operation is currently in progress Hay otra operación en progreso actualmente Please wait until another operation will be finished. Por favor espera hasta que la otra operación finalice. Please wait until another operation will be finised. Por favor espera hasta que la otra operación finalice. Do you really want to remove all keys from this database? ¿Seguro que quieres borrar todas las claves de esta base de datos? Cannot load databases: No se pueden cargar bases de datos: Live update was disabled Actualización automática se ha desactivado Live update was disabled due to exceeded keys limit. Please specify filter more carrfully or change limit in settings. Actualización automática se ha desactivado al superarse el límite de claves. Por favor especifica un filtro más restrictivo o cambia el límite en ajustes. Rename key Renombrar la clave New name: Nuevo nombre: Total pages: Total de páginas: Size: Tamaño: TTL: TTL: Set key TTL Ajusta TTL de la clave New TTL: Nuevo TTL: Delete Borrar Delete key Borrar Clave Changes are not saved Cambios no guardados Do you want to close key tab without saving changes? ¿Quieres cerrar la pestaña de claves sin guardar los cambios? Persist key Persistir clave Do you really want to delete this key? ¿Seguro que quieres borrar esta clave? Reload Value Recargar Valor Add Row Añadir Fila Add Element to HLL Añadir Elemento a HLL Add Añadir Delete row Borrar Fila The row is the last one in the key. After removing it key will be deleted. Esta fila es la última en la clave. Después de borrarla, la clave será borrada. Do you really want to remove this row? ¿Seguro que quieres borrar esta fila? Search on page... Buscar en la página... Full Search Búsqueda Completa Value and Console tabs related to this connection will be closed. Do you want to continue? Las pestañas de Valor y Consola relacionadas con esta conexión deben cerrarse. ¿Quieres continuar? Do you really want to delete connection? ¿Seguro que quieres borrar la conexión? Connected to cluster. Conectado al cluster. Connected. Conectado. Switch to %1 mode. Close console tab to stop listen for messages. Cambio a modo %1. Cierra la pestaña de consola para dejar de escuchar mensajes. Subscribe error: %1 Error de suscripción: %1 Server %0 Servidor %0 Can't find formatter with name: %1 No se encuentra un formateador con el nombre: %1 Can't find formatter: %1 No puede encontrar el formateador: %1 Invalid callback Llamada de retorno inválida Can't load list of available formatters from extension server: %1 No puede cargar la lista de formateadores del servidor de extensión: %1 Can't encode value: %1 No puede codificar el valor: %1 Cannot decode value using %1 formatter. No se puede decodificar el valor usando el formateador %1. Cannot validate value using %1 formatter. No se puede validar el valor usando el formateador %1. Cannot encode value using %1 formatter. No se puede codificar el valor usando el formateador %1. Loading key: %1 from db %2 Cargando clave: %1 desde db %2 Cannot open value tab No se puede abrir la pestaña de valores Connection error Error de conexión Connection error. Can't open value tab. Error de conexión. No se puede abrir la pestaña de valores. Cannot reload key value: %1 No se puede recargar el valor de la clave: %1 Cannot load key value: %1 No se puede cargar el valor de la clave: %1 Connect to Redis Server Conectar a servidor Redis Import Importar Import Connections Importar Conexiones Export Exportar Export Connections Exportar Conexiones Report issue Informar de un problema Documentation Documentación Join Telegram Chat Unirse al chat de Telegram Follow Seguir Star on GitHub! ¡Estrella en GitHub! Log Log Extension Server Servidor de extensión Settings Ajustes New Connection Settings Ajustes de Nueva Conexión How to connect Cómo conectar Connection Settings Ajustes de Conexión Create connection from Redis URL Crear conexión desde Redis URL Learn more about Redis URL: Aprender más acerca de Redis URL: Connection guides Guías de conexión Local or Public Redis Redis Público o Local Redis with SSL/TLS Redis con SSL/TLS SSH tunnel Túnel SSH UNIX socket Socket UNIX Cannot figure out how to connect to your redis-server? ¿No sabe cómo conectar a su servidor redis? <a href="https://docs.resp.app/en/latest/quick-start/">Read the Docs</a>, <a href="mailto:support@resp.app">Contact Support</a> or ask for help in our <a href="https://t.me/RedisDesktopManager">Telegram Group</a> <a href="https://docs.resp.app/en/latest/quick-start/">Leer la documentación</a>, <a href="mailto:support@resp.app">Contactar Soporte</a> o pedir ayuda en nuestro <a href="https://t.me/RedisDesktopManager">Grupo de Telegram</a> Don't have running Redis? ¿No tiene Redis en ejecución? Spin up hassle-free Redis on Digital Ocean Lanza Redis sin complicaciones en Digital Ocean Skip Omitir Name: Nombre: Connection Name Nombre de Conexión Address: Dirección: redis-server host host Redis Server For better network performance please use 127.0.0.1 Para obtener un mejor despempeño usa 127.0.0.1 (Optional) redis-server authentication password (Opcional) Contraseña Redis server Username: Nombre de usuario: (Optional) redis-server authentication username (Redis >6.0) (Opcional) Nombre de usuario de autenticación de redis-server (Redis >6.0) Security Seguridad Public Key: Clave Pública: (Optional) Public Key in PEM format (Opcional) Clave Pública en formato PEM Select public key in PEM format Selecciona clave pública en formato PEM (Optional) Private Key in PEM format (Opcional) Clave Privada en formato PEM Select private key in PEM format Selecciona clave privada en formato PEM Authority: Autoridad: (Optional) Authority in PEM format (Opcional) Autoridad en formato PEM Select authority file in PEM format Selecciona autoridad en formato PEM Enable strict mode: Activar modo estricto: SSH Tunnel Túnel SSH SSH Address: Dirección SSH: Remote Host with SSH server Host Remoto con servidor SSH SSH User: Usuario SSH: Valid SSH User Name Nombre de Usuario SSH Válido <b>Tip:</b> Use <code>⌘ + Shift + .</code> to show hidden files and folders in dialog <b>Tip:</b> Use <code>⌘ + Shift + .</code> para mostrar ficheros y carpetas ocultas en el diálogo Private Key Clave Privada Path to Private Key in PEM format Ruta a la Clave Privada en formato PEM Password Contraseña SSH User Password Contraseña Usuario SSH Enable TLS-over-SSH (<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) Activar TLS sobre SSH (<b>AWS ElastiCache</b> <b>Encriptación en tránsito</b>) Advanced Settings Ajustes Avanzados SSL / TLS SSL / TLS Use SSH Agent Usar Agente SSH (Optional) Custom SSH Agent Path (Opcional) Mofificar Ruta del Agente SSH Select SSH Agent Seleccionar Agente SSH Additional configuration is required to enable SSH Agent support Configuración adicional es requerida para habilitar el soporte de Agente SSH Passphrase for provided private key Contraseña para clave privada proporcionada Password request will be prompt prior to connection Se pedirá contraseña antes de la conexión Ask for password Pedir contraseña Keys loading Carga de claves Default filter: Filtro por defecto: Pattern which defines loaded keys from redis-server Patrón que define las claves cargadas desde el servidor redis Namespace Separator: Separador de Namespace: Separator used for namespace extraction from keys Separador usado para extracción de namespace de las claves Timeouts & Limits Timeouts y Límites Connection Timeout (sec): Timeout de Conexión (seg): Execution Timeout (sec): Timeout de Ejecución (seg): Databases discovery limit: Límite de descubrimiento de Bases de datos: Cluster Cluster Change host on cluster redirects: Cambiar host en redirección de cluster: Formatters Formateadores Default value formatter: Valor por defecto del formateador: Auto autodetect (JSON / Plain Text / HEX) Autodetectar (JSON / Texto Plano / HEX) Last selected Último seleccionado Select formatter ... Seleccionar formateador ... Appearance Apariencia Icon color: Color del ícono Invalid settings detected! Ajustes inválidos detectados! Test Connection Probar Conexión OK OK Cancel Cancelar General General Application will be restarted to apply these settings. La aplicación será reiniciada al aplicar las configuraciones. Language Idioma Application will be restarted to apply this setting. La aplicación se reiniciara para aplicar este ajuste. Font Fuente Font Size Tamaño Fuente Dark Mode Modo Oscuro Maximum Formatted Value Size Máximo tamaño de valor formateado Size in bytes Tamaño en bytes Use system proxy settings Usar ajustes del proxy del sistema Use system proxy only for HTTP(S) requests Usar proxy del sistema sólo para peticiones HTTP(S) Value Editor Valor de editor Maximum amount of items per page Máxima cantidad de items por página Connections Tree Árbol de conexiones Show namespaced keys on top Mostrar arriba las claves con namespace Reopen namespaces on reload Reabrir namespaces al recargar (Disable to improve treeview performance) Desactivar para mejorar el rendimiento de la vista de árbol Show only last part for namespaced keys Mostrar únicamete la última parte de claves con namespace Limit for SCAN command Límite para comando SCAN Maximum amount of rendered child items Cantidad máxima de items hijos a mostrar Live update maximum allowed keys Máximo de claves permitidas en actualización automática Live update interval (in seconds) Intervalo de actualización automática (en segundos) External Value View Formatters Formateadores Externos de Visor de Valores Formatters path: %0 Ruta a formateadores: %0 Server Url: URL del servidor Basic Auth: Autorización básica User Usuario Response timeout (in seconds) Available Data Formatters Reload Id Name Nombre Read Only Version Versión Explore RDM Explorar RDM Before using RDM take a look on the %1 Antes de usar RDM echa un vistazo en %1 Quick Start Guide Guía de inicio rápido Successful connection to redis-server Conexión correcta a servidor redis Can't connect to redis-server No se puede conectar a servidor redis Add Group Agregar grupo Regroup connections Reagrupar conexiones Exit Regroup Mode Salir del modo de reagrupación Bulk Operations Manager Administrador de Operaciones Masivas Invalid RDB path Ruta RDB inválida Please specify valid path to RDB file Por favor, especifique una ruta válida a un fichero RDB Delete keys Borrar Claves Set TTL Asignar TTL Copy keys to another database Copiar claves a otra base de datos Copy keys Copiar claves Import data from rdb file Importar datos desde fichero rdb Redis Server: Servidor Redis: Database number: Número de Base de Datos: Path to RDB file: Ruta a fichero RDB: Select DB in RDB file: Seleccione Base de Datos en fichero RDB: Key pattern: Patrón de clave: Import keys that match <b>regex</b>: Importar claves que coincidan con <b>regex</b>: Destination Redis Server: Servidor Redis Destino: Destination Redis Server Database Index: Índice Base de Datos Servidor Redis Destino: Show matched keys Mostrar claves coincidentes Show Affected keys Mostrar claves afectadas Affected keys: Claves afectadas: Matched keys: Claves coincidentes: Bulk Operation finished. Operación Masiva finalizada. Bulk Operation finished with errors Operación Masiva finalizada con errores Processed: Procesado: Getting list of affected keys... Obteniendo lista de claves afectadas... Success Correcto Confirmation Confirmación Do you really want to perform bulk operation? ¿De verdad quieres realizar la operación masiva? Sign in with resp.app account Identificarse con una cuenta resp.app Renew your subscription Renueva tu suscripción Your trial has ended. Tu periodo de prueba ha finalizado. You have no active subscription No tienes una suscripción activa No internet connection No hay conexión a Internet Your trial has ended Tu periodo de prueba ha finalizado To use this version you need to renew your subscription. Para usar esta version necesitas renovar tu suscripción. Please make sure that RDM is not blocked by a firewall and you have an internet connection. Asegúrate de que RDM no está bloqueado y tienes conexión a Internet If you’re behind a proxy please enable Actívalo si estás detrás de un Proxy option before sign-in. opción antes de iniciar sesión Please purchase a subscription to continue using RDM. Por favor compra una suscripción para seguir usando RDM. Sign in with RESP.app account Please make sure that RESP.app is not blocked by a firewall and you have an internet connection. Please purchase a subscription to continue using RESP.app. If you have any questions please contact support Si tienes alguna pregunta por favor contacta con soporte Renew Subscription Renovar Suscripción Buy Subscription Comprar Suscripción Try Again Inténtalo de nuevo Email: Email: Password: Contraseña: Show password Mostrar contraseña Forgot password? ¿Contraseña olvidada? Sign In Identificarse Please enter email & password to sign in. Por favor introduce email & contraseña para acceder. Offline Activation Activación Offline Paste Activation code here Pegar aquí código de Activación Where can I find my activation code? ¿Dónde puedo encontrar mi código de activación? Activate Activar Please enter valid activation code. Por favor introduzca un código de activación válido (Removed) (Borrado) Open Keys Filter Abrir Filtro de Claves Reload Keys in Database Recargar Claves en la Base de Datos Add New Key Añadir Nueva Clave Disable Live Update Desactivar Actualización Automática Enable Live Update Activar Actualización Automática Open Console Abrir Consola Analyze Used Memory Analizar Memoria Usada Bulk Operations Operaciones Masivas Flush Database Vaciar Base de Datos Delete keys with filter Borrar claves con filtro Set TTL for multiple keys Asignar múltiples llaves TTL Copy keys from this database to another Copiar claves desde esta base de datos a otra Import keys from RDB file Importar claves desde fichero RDB Back Atrás Copy Key Name Copiar Nombre de Clave Reload Namespace Recargar Namespace Copy Namespace Pattern Copiar Patrón de Namespace Delete Namespace Borrar Namespace Disconnect Desconectar Server Info Información Servidor Reload Server Recargar Servidor Unload All Data Descargar Todos los Datos Edit Connection Settings Editar Ajustes de Conexión Duplicate Connection Duplicar Conexión Delete Connection Borrar Conexión Connecting... Conectando... Clear Limpiar Arguments Argumentos Description Descripción Available since Disponible desde Close Cerrar View Server Info Redis Version Versión Redis Used memory Memoria usada Cmd Processed Monitor Commands Clients Clientes Server Actions Commands Processed Comandos Procesados Uptime Tiempo activo Total Keys Total de Claves Hit Ratio Ratio de Aciertos Server Stats Console day(s) día(s) Info Info Commands Per Second Comandos Por Segundo Ops/s Ops/s Connected Clients Clientes Conectados Memory Usage Uso de Memoria Mb Mb Network Input Entrada de Red Kb/s Kb/s Network Output Salida de Red Total Error Replies Error Replies Keys Claves Auto Refresh Auto Refrescar Property Propiedad Value Valor Subscribe in Console Suscribirse en la consola Slowlog Slowlog Pub/Sub Channels Canales Pub/Sub Execution Time (μs) Tiempo de Ejecución (μs) Enable Activar Channel Name Nombre de Canal Command Comando Processed at Procesado en Client Address Dirección del Cliente Age (sec) Antigüedad (seg) Idle Inactivo Flags Flags Current Database Base de Datos Actual Add New Key to Añadir Nueva Clave a Key: Clave: Type: Tipo: Or Import Value from the file O importar valor desde el fichero (Optional) Any file (Opcional) Cualquier fichero Select file with value Seleccionar fichero con valor Save Guardar Edit Connections Group Editar grupo de conexión Add New Connections Group Agregar nuevo grupo de conexión Group Name: Nombre del grupo: Error Error Page Página Enter valid value Introduzca un valor válido Formatting error Error de Formateo Unknown formatter error (Empty response) Error desconocido del formateador (Respuesta vacía) [Binary] [Binario] [Compressed: [Comprimido: Copy to Clipboard Copiar al Portapapeles Exit Full Screen Mode Save Changes Guardar Cambios Search string Buscar cadena Find Next Encontrar Siguiente Find Encontrar Regex Regex Cannot find more results No se puede encontrar más resultados Try to decompress: Intento de descomprimir: Decompressed: Descomprimido: Cannot decompress value using No se puede descomprimir valor usando Cannot find any results No se puede encontrar ningún resultado Binary value is too large to display El valor binario es demasiado grande para mostrarse View as: Ver como: Large value (>150kB). Formatters are not available. Valor grande (>150kB). Formateadores no disponibles. Score Score Path to dump.rdb file Ruta al fichero dump.rdb Select dump.rdb Selecciona dump.rdb ID ID Value (represented as JSON object) Valor (representado como objeto JSON) The row has been changed on server.Reload and try again. La fila ha cambiado en el servidor. Recarga e inténtalo de nuevo. Failed to perform actions on %1 keys. Fallo al realizar acciones en las claves: %1. Cannot copy key No se puede copiar la clave Source connection error Error en la conexión de partida Target connection error Error en la conexión de destino Cannot remove key No se puede borrar la clave Cannot execute command No se puede ejecutar el comando Invalid regexp for keys filter. Regexp inválido para filtro de claves Cannot get the list of affected keys No se puede obtener la lista de claves afectadas Cannot set TTL for key No se puede asignar el TTL a la clave Your redis-server doesn't support <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a> commands. El servidor redis no soporta comandos <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a>. Key was added. Do you want to reload keys in selected namespace? Clave añadida. ¿Quiere recargar las claves en el namespace seleccionado? Network is not accessible. Please ensure that you have internet access and try again. Red no accesible. Por favor comprueba que tienes acceso a Internet y prueba otra vez. Invalid login or password Login o password inválidos Too many requests from your IP Demasiadas peticiones desde tu IP Unknown error. Status code %1 Error desconocido. Código estado %1 Cannot parse server reply No se puede interpretar la respuesta del servidor Cannot validate token No se puede validar token Cannot login - %1. <br/> Please try again or contact <a href='mailto:support@resp.app'>support@resp.app</a> No se puede iniciar sesión - %1. <br/> Por favor, inténtalo de nuevo o contacta con <a href='mailto:support@resp.app'>support@resp.app</a> Expired activation code Invalid activation code Cannot save the update. Disk is full or download folder is not writable. No se puede guardar la actualización. Disco lleno o no se puede escribir en la carpeta. Download was canceled Se canceló la descarga Network error Error de red Select File Seleccionar Fichero Save to File Guardar a Fichero Save Value Guardar Valor Save value to file Guardar valor a fichero Save Raw Value to File Guardar Valor en Bruto en Fichero Save Formatted Value to File Guardar Valor Formateado en Fichero Save Raw Value Guardar Valor en Bruto Save Formatted Value Guardar Valor Formateado Save raw value to file Guardar Valor Formateado en Fichero Save formatted value to file Guardar Valor Formateado en Fichero Value was saved to file: Valor guardado en fichero: Cannot connect to redis-server No se puede conectar al servidor Redis Edit Connection Group Editar grupo de conexión Delete Connection Group Borrar grupo de conexión Do you really want to delete group <b>with all connections</b>? ¿Realmente deseas borrar el grupo <b>con todas sus conexiones</b>? Order of elements: Órden de elementos: Default Por defecto Reverse Invertir Start date should be less than End date Fecha de inicio debe ser menor a la fecha de fin Apply filter Aplicar filtro <span style="font-size: 11px;">Powered by awesome <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">open-source software</a> and <a href="http://icons8.com/">icons8</a>.</span> <span style="font-size: 11px;">Impulsado por asombroso <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">software open-source</a> y <a href="http://icons8.com/">icons8</a>.</span> Trial is active till Prueba activa hasta Licensed to Licenciado a Subscription is active until: Suscripción activa hasta: Manage Subscription Administrar Suscripción Getting Started Empezar Thank you for choosing RDM. Let's make your Redis experience better. Gracias por elegir RDM Thank you for choosing RESP.app. Let's make your Redis experience better. Connect to Redis-Server Conectar a Servidor Redis Read the Docs Leer la Documentación Load more keys Cargar más claves Yes No No SSH Passphrase Contraseña SSH Unknown Desconocido Passphrase Contraseña Continue Continuar Select Unsupported Redis Data type Cannot delete key: ================================================ FILE: src/resources/translations/rdm_ja_JP.ts ================================================ QObject Cannot connect to cluster node %1:%2 クラスタノードに書き込みできません %1:%2 Cannot flush db (%1): %2 DBをフラッシュできません (%1): %2 RESP Settings directory is not writable 設定したディレクトリは書き込みできません RESP.app can't save connections file to settings directory. Please change file permissions or restart RESP.app as administrator. Cannot parse scan response スキャンのレスポンスをパースできません Server returned unexpected response: サーバが不正なレスポンスを返しました: Cannot set TTL for key %1 キー%1にTTLを設定できません Cannot rename key %1: %2 キー%1を変更できません: %2 Cannot persist key '%1'. <br> Key does not exist or does not have an assigned TTL value Cannot load rows for key %1: %2 キー%1のROWを読むことができません: %2 Invalid row 不正なROWです Value with the same key already exists Connection error: 接続エラー: Data was loaded from server partially. サーバからデータが部分的にロードされました。 Cannot load key %1, connection error occurred: %2 キー%1を読めません。接続エラーが発生しました: %2 Cannot load key %1 because it doesn't exist in database. Please reload connection tree and try again. データベースに存在しないためキー%1を読めません。接続ツリーをリロードしてから改めて試してください。 Cannot load TTL for key %1, connection error occurred: %2 キー%1のTTLがロードできません。接続エラーが発生しました: %2 Cannot retrieve type of the key: Cannot open file with key value Cannot connect to server '%1'. Check log for details. サーバ'%1'に接続できません。詳細はログを確認してください。 Open Source version of RESP.app <b>doesn't support SSH tunneling</b>.<br /><br /> To get fully-featured application, please buy subscription on <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Every single subscription gives us funds to continue the development process and provide support to our users. <br />If you have any questions please feel free to contact us at <a href='mailto:support@resp.app'>support@resp.app</a> or join <a href='https://t.me/RedisDesktopManager'>Telegram chat</a>. Cannot load keys: %1 キーをロードできません: %1 Delete key error: %1 キー削除エラー: %1 Cannot determine amount of used memory by key: %1 Cannot flush database: データベースをフラッシュできません: Invalid Connection. Check connection settings. 不正な接続です。接続の設定を確認してください。 Live update was disabled due to exceeded keys limit. Please specify filter more carefully or change limit in settings. キーの上限を超えているためライブアップデートは無効です。より適切なフィルタを指定するか、または設定で上限を変更してください。 Key was added. Do you want to reload keys in selected database? キーを追加しました。選択したデータベースのキーをリロードしますか? Key was added キーを追加しました。 Another operation is currently in progress 別の処理が動作しています。 Please wait until another operation will be finished. 処理が終わるまでお待ちください。 Please wait until another operation will be finised. 処理が終わるまでお待ちください。 Do you really want to remove all keys from this database? このデータベースから全てのキーを削除しても本当によろしいですか? Cannot load databases: データベースをロードできません: Live update was disabled ライブ・アップデートは無効です Live update was disabled due to exceeded keys limit. Please specify filter more carrfully or change limit in settings. キーの上限を超えているためライブアップデートは無効です。より適切なフィルタを指定するか、または設定で上限を変更してください。 Rename key キー名の変更 New name: 新しい名前: Total pages: 総ページ数: Size: サイズ: TTL: TTL: Set key TTL キーのTTLを設定 New TTL: 新しいTTL: Delete 削除 Delete key キーを削除 Changes are not saved 変更は保存されていません Do you want to close key tab without saving changes? 変更を保存せずにキーのタブを閉じますか? Persist key 永続化キー Do you really want to delete this key? このキーを本当に削除してもよろしいですか? Reload Value 値をリロード Add Row ROWを追加 Add Element to HLL HLLに要素を追加 Add 追加 Delete row ROWを削除 The row is the last one in the key. After removing it key will be deleted. このROWはこのキーの最後の1つです。削除後はキーも削除されます。 Do you really want to remove this row? このROWを本当に削除しますか? Search on page... ページを検索... Full Search すべて検索 Value and Console tabs related to this connection will be closed. Do you want to continue? この接続に関連する値とコンソールのタブを閉じます。続行しますか? Do you really want to delete connection? 接続を本当に削除しますか? Connected to cluster. クラスタに接続しました。 Connected. 接続しました。 Switch to %1 mode. Close console tab to stop listen for messages. %1モードに変更する。メッセージを見るのをやめるにはコンソールのタブを閉じてください。 Subscribe error: %1 サブスクライブエラー: %1 Server %0 サーバ %0 Can't find formatter with name: %1 指定したフォーマッタが見つかりません: %1 Can't find formatter: %1 Invalid callback 不正なコールバックです Can't load list of available formatters from extension server: %1 Can't encode value: %1 Cannot decode value using %1 formatter. フォーマッタ%1で値をデコードすることができません。 Cannot validate value using %1 formatter. フォーマッタ%1で値を検証することができません。 Cannot encode value using %1 formatter. フォーマッタ%1で値をエンコードすることができません。 Loading key: %1 from db %2 データをロード: DB %2 の %1 Cannot open value tab 値タブを開くことができません Connection error Connection error. Can't open value tab. 接続エラー。値タブを開けません。 Cannot reload key value: %1 キーの値をリロードできません: %1 Cannot load key value: %1 キーの値をロードできません: %1 Connect to Redis Server Redisサーバに接続 Import インポート Import Connections 接続情報のインポート Export エクスポート Export Connections 接続情報のエクスポート Report issue 問題を報告 Documentation ドキュメント Join Telegram Chat Telegram Chatに参加 Follow フォローする Star on GitHub! GitHubで貢献する! Log ログ Extension Server Settings 設定 New Connection Settings 新しい接続の設定 How to connect Connection Settings 接続の設定 Create connection from Redis URL Learn more about Redis URL: Connection guides Local or Public Redis Redis with SSL/TLS SSH tunnel UNIX socket Cannot figure out how to connect to your redis-server? <a href="https://docs.resp.app/en/latest/quick-start/">Read the Docs</a>, <a href="mailto:support@resp.app">Contact Support</a> or ask for help in our <a href="https://t.me/RedisDesktopManager">Telegram Group</a> Don't have running Redis? Spin up hassle-free Redis on Digital Ocean Skip Name: 名前: Connection Name 接続名 Address: アドレス: redis-server host Redisサーバのホスト For better network performance please use 127.0.0.1 127.0.0.1を使うとネットワークのパフォーマンスが向上します (Optional) redis-server authentication password (任意) Redisサーバ認証パスワード Username: ユーザー名: (Optional) redis-server authentication username (Redis >6.0) (任意) Redisサーバ認証ユーザー名 (Redis >6.0) Security セキュリティ Public Key: 公開鍵: (Optional) Public Key in PEM format (任意) PEM形式の公開鍵 Select public key in PEM format PEM形式の公開鍵を選択してください (Optional) Private Key in PEM format (任意) PEM形式の非公開鍵 Select private key in PEM format PEM形式の非公開鍵を選択してください Authority: 証明書: (Optional) Authority in PEM format (任意) PEM形式の証明書 Select authority file in PEM format PEM形式の証明書を選択してください SSH Tunnel SSHトンネル SSH Address: SSHアドレス Remote Host with SSH server SSHサーバのリモートホスト SSH User: SSHユーザー: Valid SSH User Name 有効なSSHユーザー名 Private Key 非公開鍵 Path to Private Key in PEM format PEM形式の非公開鍵のファイルパス <b>Tip:</b> Use <code>⌘ + Shift + .</code> to show hidden files and folders in dialog <b>Tip:</b> <code>⌘ + Shift + .</code>で隠しファイルや隠しフォルダをダイアログに表示できます Password パスワード SSH User Password SSHユーザーのパスワード Enable TLS-over-SSH (<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) TLS-over-SSHを有効にする。(<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) Advanced Settings 詳細設定 Sign in with resp.app account resp.appのアカウントでサインイン Sign in with RESP.app account Renew your subscription You have no active subscription No internet connection Your trial has ended To use this version you need to renew your subscription. Please make sure that RESP.app is not blocked by a firewall and you have an internet connection. If you’re behind a proxy please enable option before sign-in. Please purchase a subscription to continue using RESP.app. If you have any questions please contact support Renew Subscription Buy Subscription Try Again Email: Email: Password: パスワード: Forgot password? Application will be restarted to apply this setting. 設定を有効にするためアプリケーションは再起動されます Sign In サインイン Please enter email & password to sign in. Offline Activation Paste Activation code here Where can I find my activation code? Activate Please enter valid activation code. SSL / TLS SSL / TLS Enable strict mode: ストリクトモードを有効にする Use SSH Agent (Optional) Custom SSH Agent Path Select SSH Agent Additional configuration is required to enable SSH Agent support Passphrase for provided private key Password request will be prompt prior to connection Ask for password Keys loading キーの読み込み Default filter: 規定のフィルタ: Pattern which defines loaded keys from redis-server Redisサーバからロードするキー定義のパターン Namespace Separator: ネームスペースのセパレータ: Separator used for namespace extraction from keys キーから抽出するネームスペースに使用するセパレータ Timeouts & Limits タイムアウトと上限 Connection Timeout (sec): 接続タイムアウト(秒) Execution Timeout (sec): 実行タイムアウト(秒) Databases discovery limit: データベース探索リミット: Cluster クラスタ Change host on cluster redirects: クラスタをリダイレクトするホストを変更: Formatters Default value formatter: Auto detect (JSON / Plain Text / HEX) Last selected Select formatter ... Appearance Icon color: Invalid settings detected! 不正な設定を検出しました! Test Connection 接続テスト OK OK Cancel キャンセル General 一般 Application will be restarted to apply these settings. アプリケーションを再起動すると設定が有効になります。 Language 言語 Font フォント Font Size フォントサイズ Dark Mode Maximum Formatted Value Size フォーマット済み値の最大サイズ Size in bytes バイト数 Use system proxy settings OSのプロキシ設定を使う Use system proxy only for HTTP(S) requests HTTP(S)にシステムのプロキシ設定のみを使う Value Editor 値エディタ Maximum amount of items per page Connections Tree 接続ツリー Show namespaced keys on top ネームスペース付きのキーを上に表示 Reopen namespaces on reload リロード時にネームスペースを開きなおす (Disable to improve treeview performance) (無効にするとツリービューが早くなります) Show only last part for namespaced keys ネームスペース付きのキーの末尾のみを表示 Limit for SCAN command Maximum amount of rendered child items Live update maximum allowed keys ライブアップデートで読み込むキーの最大数 Live update interval (in seconds) ライブアップデートの更新頻度(秒) External Value View Formatters 外部の値ビューフォーマッタ Formatters path: %0 フォーマッタのパス: %0 Server Url: Basic Auth: User Response timeout (in seconds) Available Data Formatters Reload Id Name 名称 Read Only Version バージョン Quick Start Guide クイックスタート・ガイド Successful connection to redis-server Redisサーバへの接続に成功 Can't connect to redis-server Redisサーバに接続できません Add Group グループを追加 Regroup connections 接続グループの編集 Exit Regroup Mode 接続グループ編集モードを終了 Show password パスワードを表示 (Removed) (削除済) Open Keys Filter キーフィルタを開く Reload Keys in Database データベースのキーをリロード Add New Key キーを追加 Disable Live Update ライブアップデートを無効化 Enable Live Update ライブアップデートを有効化 Open Console コンソールを開く Analyze Used Memory メモリを分析 Bulk Operations バッチ処理 Flush Database データベースを初期化 Delete keys with filter フィルタを用いてキーを削除 Set TTL for multiple keys 複数のキーにTTLを設定 Copy keys from this database to another このデータベースから別のデータベースへキーをコピー Import keys from RDB file RDBファイルからキーをインポート Back 戻る Copy Key Name キー名をコピー Reload Namespace ネームスペースをリロード Copy Namespace Pattern ネームスペースのパターンをコピー Delete Namespace ネームスペースを削除 Disconnect 接続終了 Server Info サーバ情報 Reload Server サーバをリロード Unload All Data 全てのデータをアンロード Edit Connection Settings 接続情報を編集 Duplicate Connection 接続情報をコピー Delete Connection 接続情報を削除 Connecting... 接続中... Clear 消去 Arguments パラメーター Description 説明 Available since 利用可能 Close 閉じる View Server Info Redis Version Redisバージョン Used memory 消費メモリ Cmd Processed Monitor Commands Clients クライアント数 Server Actions Commands Processed 実行コマンド数 Uptime 稼働時間 Total Keys 総キー数 Hit Ratio ヒット率 Server Stats Console day(s) Info 情報 Commands Per Second コマンド数/秒 Ops/s OP数/秒 Connected Clients クライアント接続数 Memory Usage メモリ消費量 Mb MB Network Input ネットワーク入力 Kb/s KB/秒 Network Output ネットワーク出力 Total Error Replies Error Replies Keys キー Auto Refresh 自動リフレッシュ Property プロパティ Value Subscribe in Console コンソール上でサブスクライブ Slowlog Slowlog Pub/Sub Channels Pub/Subチャンネル Enable 有効 Channel Name チャンネル名 Command コマンド Processed at 処理 Execution Time (μs) 実行時間(μs) Client Address クライアントのアドレス Age (sec) 経過時間(秒) Idle アイドル Flags フラグ Current Database 現在のデータベース Add New Key to 新しいキーを追加 Key: キー: Type: 型: Or Import Value from the file (Optional) Any file Select file with value Save 保存 Edit Connections Group 接続グループを編集 Add New Connections Group 接続グループの新規追加 Group Name: グループ名: Error エラー Page ページ Enter valid value 有効な値を入力 Formatting error フォーマットエラー Unknown formatter error (Empty response) 予期せぬフォーマッタのエラー (空の応答) [Binary] [バイナリ] [Compressed: [圧縮: Copy to Clipboard クリップボードにコピー Exit Full Screen Mode Save Changes 変更を保存 Search string Find Next Find Regex Cannot find more results Try to decompress: Decompressed: Cannot decompress value using Cannot find any results Binary value is too large to display バイナリが大きすぎるため表示できません View as: 表示形式: Large value (>150kB). Formatters are not available. 値が大きすぎます(>150kB)。フォーマッタは使用できません。 Score ソース Bulk Operations Manager バッチ処理マネージャ Invalid RDB path 不正なRDBのパスです Please specify valid path to RDB file RDBファイルへの正しいパスを指定してください Delete keys キーを削除 Set TTL TTLを設定 Copy keys to another database 他のデータベースにキーをコピー Copy keys キーをコピー Import data from rdb file RDBファイルからデータをインポート Redis Server: Redisサーバ: Database number: データベース番号: Path to RDB file: RDBファイルへのパス: Path to dump.rdb file dump.rdbファイルへのパス Select dump.rdb dump.rdbを選択 Select DB in RDB file: RDBファイルからDBを選択 Import keys that match <b>regex</b>: <b>正規表現</b>に一致するキーをインポート: Key pattern: キーパターン: Destination Redis Server: Redisサーバの接続先: Destination Redis Server Database Index: Redisデータベースインデックスの接続先: Show matched keys 一致するキーを表示 Show Affected keys 影響するキーを表示 Matched keys: 一致したキー: Affected keys: 影響するキー: Bulk Operation finished. バッチ処理が完了しました。 Bulk Operation finished with errors バッチ処理でエラーが発生しました Processed: Getting list of affected keys... Success Confirmation 確認 Do you really want to perform bulk operation? バッチ処理を本当に実行しますか? ID ID Value (represented as JSON object) 値(JSONオブジェクト形式) The row has been changed on server.Reload and try again. サーバ上のROWが更新されました。リロードしてからやりなおしてください。 Failed to perform actions on %1 keys. %1キーに対する処理が失敗しました Cannot copy key キーをコピーできません Source connection error ソース接続エラー Target connection error ターゲット接続エラー Cannot remove key キーを削除できません Cannot execute command コマンドを実行できません Invalid regexp for keys filter. Cannot get the list of affected keys Cannot set TTL for key TTLをキーに設定できません Your redis-server doesn't support <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a> commands. あなたのRedisサーバは<a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a>コマンドをサポートしていません。 Key was added. Do you want to reload keys in selected namespace? キーを追加しました。選択されているネームスペースのキーをリロードしますか? Select File ファイルを選択 Save to File ファイルに保存 Save Value 値を保存 Save value to file 値をファイルに保存 Save Raw Value to File Save Formatted Value to File Save Raw Value Save Formatted Value Value was saved to file: 値をファイルに保存しました: Cannot connect to redis-server Redisサーバに接続できません Edit Connection Group 接続グループの編集 Delete Connection Group 接続グループの削除 Do you really want to delete group <b>with all connections</b>? グループを本当に削除しますか? <b>グループの接続情報もすべて失われます</b> Order of elements: 要素の順序: Default デフォルト Reverse 降順 Start date should be less than End date 開始日は終了日より前でなければなりません Apply filter フィルタを適用 Network is not accessible. Please ensure that you have internet access and try again. Invalid login or password Too many requests from your IP Unknown error. Status code %1 Cannot parse server reply Cannot validate token Cannot login - %1. <br/> Please try again or contact <a href='mailto:support@resp.app'>support@resp.app</a> Cannot save the update. Disk is full or download folder is not writable. Download was canceled ダウンロードをキャンセルしました Network error ネットワークエラー Expired activation code Invalid activation code <span style="font-size: 11px;">Powered by awesome <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">open-source software</a> and <a href="http://icons8.com/">icons8</a>.</span> Getting Started Thank you for choosing RESP.app. Let's make your Redis experience better. Connect to Redis-Server Read the Docs Load more keys SSH Passphrase Unknown Passphrase Continue Yes No Trial is active till Licensed to Subscription is active until: Manage Subscription Select Unsupported Redis Data type サポートしていないRedisのデータ型です Cannot delete key: ================================================ FILE: src/resources/translations/rdm_uk_UA.ts ================================================ QObject Cannot connect to cluster node %1:%2 Не вдається підключитися до вузла кластера %1:%2 Cannot flush db (%1): %2 Не вдається очистити базу даних (%1): %2 RESP Settings directory is not writable Каталог з налаштуваннями недоступний для запису RESP.app can't save connections file to settings directory. Please change file permissions or restart RESP.app as administrator. RDM can't save connections file to settings directory. Please change file permissions or restart RDM as administrator. RDM не може зберегти файл підключень до каталогу налаштувань. Змініть файлові дозволи або перезапустіть RDM як адміністратор. Cannot parse scan response Не вдається розібрати відповідь на команду SCAN Server returned unexpected response: Сервер повернув несподівану відповідь: Cannot set TTL for key %1 Не вдається встановити TTL для ключа %1 Cannot rename key %1: %2 Не вдається перейменувати ключ %1: %2 Cannot persist key '%1'. <br> Key does not exist or does not have an assigned TTL value Не вдається зробити ключ '%1' постійним. <br> Ключ не існує або не має присвоєного значення TTL Cannot load rows for key %1: %2 Не вдається завантажити рядки для ключа %1: %2 Invalid row Неприпустимий рядок Value with the same key already exists Значення з таким самим ключем вже існує Connection error: Помилка підключення: Data was loaded from server partially. Дані були завантажені з сервера частково. Cannot load key %1, connection error occurred: %2 Не вдається завантажити ключ %1, сталася помилка підключення: %2 Cannot load key %1 because it doesn't exist in database. Please reload connection tree and try again. Не вдається завантажити ключ %1, оскільки він не існує в базі даних. Перезавантажте дерево підключення та повторіть спробу. Cannot load TTL for key %1, connection error occurred: %2 Не вдається завантажити TTL для ключа %1, сталася помилка підключення: %2 Cannot retrieve type of the key: Не вдається отримати тип ключа: Cannot open file with key value Unsupported Redis Data type %1 Тип даних Redis %1 не підтримується Cannot retrive type of the key: Не вдається отримати тип ключа: Cannot connect to server '%1'. Check log for details. Не вдається підключитися до сервера '%1'. Перевірте журнал для деталей. Open Source version of RDM <b>doesn't support SSH tunneling</b>.<br /><br /> To get fully-featured application, please buy subscription on <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Every single subscription gives us funds to continue the development process and provide support to our users. <br />If you have any questions please feel free to contact us at <a href='mailto:support@resp.app'>support@resp.app</a> or join <a href='https://t.me/RedisDesktopManager'>Telegram chat</a>. Версія RDM з відкритим кодом <b>не підтримує тунелювання SSH</b>.<br /><br /> Щоб отримати повнофункціональну програму, придбайте підписку на <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Кожна окрема підписка дає нам кошти для продовження процесу розробки та надання підтримки нашим користувачам. <br />Якщо у вас виникли запитання, будь ласка, зв'яжіться з нами за адресою <a href='mailto:support@resp.app'>support@resp.app</a> або приєднуйтеся до <a href='https://t.me/RedisDesktopManager'>Telegram-чату</a>. Open Source version of RESP.app <b>doesn't support SSH tunneling</b>.<br /><br /> To get fully-featured application, please buy subscription on <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Every single subscription gives us funds to continue the development process and provide support to our users. <br />If you have any questions please feel free to contact us at <a href='mailto:support@resp.app'>support@resp.app</a> or join <a href='https://t.me/RedisDesktopManager'>Telegram chat</a>. Cannot load keys: %1 Не вдається завантажити ключі: %1 Delete key error: %1 Помилка видалення ключа: %1 Cannot determine amount of used memory by key: %1 Не вдається визначити обсяг використаної пам'яті за ключем: %1 Cannot flush database: Не вдається очистити базу даних: Invalid Connection. Check connection settings. Недійсне з’єднання. Перевірте налаштування підключення. Live update was disabled due to exceeded keys limit. Please specify filter more carefully or change limit in settings. Автоматичне оновлення було вимкнено через перевищення обмеження ключів. Будь ласка, вказуйте фільтр обережніше або змініть обмеження в налаштуваннях. Key was added. Do you want to reload keys in selected database? Ключ додано. Хочете перезавантажити ключі в обраній базі даних? Key was added Ключ додано Another operation is currently in progress Інша операція вже обробляється Please wait until another operation will be finished. Зачекайте, доки не буде закінчена інша операція. Do you really want to remove all keys from this database? Ви дійсно хочете видалити всі ключі з цієї бази даних? Cannot load databases: Не вдається завантажити бази даних: Live update was disabled Автоматичне оновлення було вимкнено Rename key Перейменувати ключ New name: Нова назва: Total pages: Всього сторінок: Size: Розмір: TTL: TTL: Set key TTL Встановити TTL ключа New TTL: Новий TTL: Delete Видалити Delete key Видалити ключ Changes are not saved Зміни не зберігаються Do you want to close key tab without saving changes? Ви хочете закрити вкладку з ключем без збереження змін? Persist key Зробити ключ постійним Do you really want to delete this key? Ви дійсно хочете видалити цей ключ? Reload Value Перезавантажити значення Add Row Додати рядок Add Element to HLL Додати елемент до HLL Add Додати Delete row Видалити рядок The row is the last one in the key. After removing it key will be deleted. Рядок - останній у ключі. Після його видалення ключ буде видалений. Do you really want to remove this row? Ви дійсно хочете видалити цей рядок? Search on page... Шукати на сторінці... Full Search Повний пошук Value and Console tabs related to this connection will be closed. Do you want to continue? Вкладки значень та консоль, пов’язані з цим з’єднанням, будуть закриті. Ви хочете продовжити? Do you really want to delete connection? Ви дійсно хочете видалити підключення? Connected to cluster. Підключено до кластера. Connected. Підключено. Switch to %1 mode. Close console tab to stop listen for messages. Switch to Pub/Sub mode. Close console tab to stop listen for messages. Перехід у режим Pub/Sub. Закрийте вкладку консолі, щоб зупинити прослуховування повідомлень. Subscribe error: %1 Помилка підписки на канал: %1 Server %0 Сервер %0 Can't find formatter with name: %1 Не вдається знайти форматер із назвою: %1 Can't find formatter: %1 Invalid callback Недійсний зворотний виклик Can't load list of available formatters from extension server: %1 Can't encode value: %1 Cannot decode value using %1 formatter. Не вдається декодувати значення за допомогою форматера %1. Cannot validate value using %1 formatter. Неможливо перевірити значення за допомогою форматера %1. Cannot encode value using %1 formatter. Не вдається закодувати значення за допомогою форматера %1. Loading key: %1 from db %2 Завантажується ключ: %1 з БД %2 Cannot open value tab Не вдається відкрити вкладку значення Connection error Connection error. Can't open value tab. Помилка підключення. Не вдається відкрити вкладку значення. Cannot reload key value: %1 Не вдається перезавантажити значення ключа: %1 Cannot load key value: %1 Не вдається завантажити значення ключа: %1 Connect to Redis Server Підключіться до сервера Redis Import Імпорт Import Connections Імпортувати підключення Export Експорт Export Connections Експорт підключень Report issue Повідомити про баг Documentation Документація Join Telegram Chat Приєднуйтесь до чату Telegram Follow Слідкувати Star on GitHub! Підтримати на GitHub! Log Журнал Extension Server Settings Налаштування New Connection Settings Налаштування нового підключення Connection Wizard Майстер підключення Connection Settings Налаштування підключення Create connection from Redis URL Створити підключення з Redis URL Learn more about Redis URL: Дізнайтеся більше про Redis URL: Connection guides Інструкції з підключення Local or Public Redis Локальний або публічний Redis Redis with SSL/TLS Redis із SSL/TLS SSH tunnel Тунель SSH UNIX socket Сокет UNIX Cannot figure out how to connect to your redis-server? Не можете зрозуміти, як підключитися до вашого Redis-сервера? <a href="https://docs.resp.app/en/latest/quick-start/">Read the Docs</a>, <a href="mailto:support@resp.app">Contact Support</a> or ask for help in our <a href="https://t.me/RedisDesktopManager">Telegram Group</a> <a href="https://docs.resp.app/en/latest/quick-start/">Ознайомтеся з документацією</a>, <a href="mailto:support@resp.app">зверніться за підтримкою</a> або зверніться за допомогою до нашої <a href="https://t.me/RedisDesktopManager">Telegram-групи</a> Don't have running Redis? Не маєте запущеного Redis-сервера? Spin up hassle-free Redis on Digital Ocean Запустити Redis на Digital Ocean Skip Пропустити Name: Назва: Connection Name Назва підключення Address: Адреса: redis-server host Хост redis-сервера For better network performance please use 127.0.0.1 Для кращої роботи мережі використовуйте 127.0.0.1 (Optional) redis-server authentication password (Необов’язково) Пароль автентифікації сервера redis Username: Ім'я користувача: (Optional) redis-server authentication username (Redis >6.0) (Необов’язково) ім’я користувача для автентифікації сервера redis (Redis> 6.0) Security Безпека Public Key: Відкритий ключ: (Optional) Public Key in PEM format (Необов’язково) Відкритий ключ у форматі PEM Select public key in PEM format Виберіть відкритий ключ у форматі PEM (Optional) Private Key in PEM format (Необов’язково) Закритий ключ у форматі PEM Select private key in PEM format Виберіть закритий ключ у форматі PEM Authority: АЦСК: (Optional) Authority in PEM format (Необов’язково) Файл АЦСК у форматі PEM Select authority file in PEM format Виберіть файл АЦСК у форматі PEM SSH Tunnel SSH тунель SSH Address: Адреса SSH: Remote Host with SSH server Віддалений хост із сервером SSH SSH User: Користувач SSH: Valid SSH User Name Дійсне ім'я користувача SSH Private Key Приватний ключ Import connection parameters from Redis connection string Імпортувати параметри з'єднання із рядка з'єднання Redis Path to Private Key in PEM format Шлях до приватного ключа у форматі PEM <b>Tip:</b> Use <code>⌘ + Shift + .</code> to show hidden files and folders in dialog <b> Порада: </b> Використовуйте <code> ⌘ + Shift +. </code>, щоб показати приховані файли та папки у діалоговому вікні Password Пароль SSH User Password Пароль користувача SSH Enable TLS-over-SSH (<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) Увімкнути TLS-over-SSH (<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) Advanced Settings Розширені налаштування Sign in with resp.app account Увійдіть за допомогою облікового запису resp.app Renew your subscription Поновити підписку Your trial has ended. Ваш пробний період закінчився. You have no active subscription У вас немає активної підписки No internet connection Немає підключення до інтернету Your trial has ended Ваш пробний період закінчився To use this version you need to renew your subscription. Для використання цієї версії вам потрібно поновити підписку. Please make sure that RDM is not blocked by a firewall and you have an internet connection. Будь ласка, переконайтеся, що RDM не заблоковано брандмауером, а також перевірте наявність підключення до інтернету. If you’re behind a proxy please enable Якщо ви за проксі-сервером, увімкніть option before sign-in. опцію перед входом. Please purchase a subscription to continue using RDM. Будь ласка, придбайте передплату, щоб продовжити використання RDM. Sign in with RESP.app account Please make sure that RESP.app is not blocked by a firewall and you have an internet connection. Please purchase a subscription to continue using RESP.app. If you have any questions please contact support Якщо у вас є будь-які запитання, звертайтесь до служби підтримки Renew Subscription Поновити підписку Buy Subscription Купити підписку Try Again Спробувати ще раз Email: Електронна адреса: Password: Пароль: Forgot password? Забули пароль? Offline Activation Paste Activation code here Where can I find my activation code? Activate Please enter valid activation code. Don’t have an account? Sign up Не маєте акаунту? Зареєструватися Application will be restarted to apply this setting. Додаток буде перезапущено, щоб застосувати цей параметр. Sign In Увійти Please enter email & password to sign in. Введіть адресу електронної пошти та пароль для входу. SSL / TLS SSL / TLS Enable strict mode: Увімкнути суворий режим: Use SSH Agent (Optional) Custom SSH Agent Path Select SSH Agent Additional configuration is required to enable SSH Agent support Passphrase for provided private key Password request will be prompt prior to connection Ask for password Keys loading Завантаження ключів Default filter: Фільтр за замовчуванням: Pattern which defines loaded keys from redis-server Шаблон, який визначає завантажені ключі з redis-сервера Namespace Separator: Розділювач простору імен: Separator used for namespace extraction from keys Розділювач для вилучення простору імен із ключів Timeouts & Limits Час очікування та ліміти Connection Timeout (sec): Час очікування підключення (сек): Execution Timeout (sec): Час очікування виконання (сек): Databases discovery limit: Ліміт на завантаження баз данних: Cluster Кластер Change host on cluster redirects: Змінювати хост при переспрямуваннях кластера: Formatters Default value formatter: Auto detect (JSON / Plain Text / HEX) Last selected Select formatter ... Appearance Icon color: Invalid settings detected! Виявлено невірні налаштування! Test Connection Тестове підключення How to connect Як підключитися OK OK Cancel Скасувати General Загальні Application will be restarted to apply these settings. Додаток буде перезапущено, щоб застосувати зміни в налаштуваннях. Language Мова Font Шрифт Font Size Розмір шрифту Dark Mode Темний режим Maximum Formatted Value Size Максимальний розмір форматованого значення Size in bytes Розмір у байтах Maximum amount of items per page Show only last part for namespaced keys Показати лише останню частину ключів з простором імен Use system proxy settings Використовувати налаштування системного проксі Use system proxy only for HTTP(S) requests Використовувати системний проксі лише для запитів HTTP(S) Value Editor Редактор значень Connections Tree Дерево підключень Show namespaced keys on top Спочатку показувати ключі з простором імен Reopen namespaces on reload Повторно відкрити простори імен при перезавантаженні (Disable to improve treeview performance) (Вимкніть, щоб пришвидшити роботу дерева ключів) Limit for SCAN command Maximum amount of rendered child items Live update maximum allowed keys Максимальна кількість ключів для автоматичного оновлення Live update interval (in seconds) Інтервал автоматичного оновлення (у секундах) External Value View Formatters Зовнішні форматери Formatters path: %0 Шлях до форматерів:%0 Server Url: Basic Auth: User Response timeout (in seconds) Available Data Formatters Reload Id Name Назва Read Only Version Версія Explore RDM Дослідіть RDM Before using RDM take a look on the %1 Перед використанням RDM ознайомтеся з %1 Quick Start Guide Інструкція для початківців Successful connection to redis-server Успішне підключення до redis-сервера Can't connect to redis-server Не вдається підключитися до redis-сервера Add Group Додати групу Regroup connections Перегрупувати підключення Exit Regroup Mode Вийти з режиму перегрупування Show password Показати пароль (Removed) (Видалено) Open Keys Filter Відкрити фільтр ключів Reload Keys in Database Перезавантажити ключі в базі даних Add New Key Додати новий ключ Disable Live Update Вимкнути автоматичне оновлення Enable Live Update Увімкнути автоматине оновлення Open Console Відкрити консоль Analyze Used Memory Проаналізувати використану пам’ять Bulk Operations Масові операції Flush Database Очистити базу даних Delete keys with filter Видалити ключі з фільтром Set TTL for multiple keys Встановити TTL для кількох ключів Copy keys from this database to another Скопіювати ключі з цієї бази даних в іншу Import keys from RDB file Імпортувати ключі з файлу rdb Back Назад Copy Key Name Копіювати назву ключа Reload Namespace Перезавантажити простір імен Copy Namespace Pattern Скопіювати шаблон простору імен Delete Namespace Видалити простір імен Disconnect Відключитися Server Info Інформація про сервер Reload Server Перезавантажити підключення Unload All Data Вивантажити всі дані Edit Connection Settings Редагувати налаштування підключення Duplicate Connection Створити дублікат підключення Delete Connection Видалити підключення Connecting... Підключення... Clear Очистити Arguments Параметри Description Опис Available since Доступно з Close Закрити View Server Info Redis Version Версія Redis Used memory Використана пам’ять Cmd Processed Monitor Commands Clients Клієнти Server Actions Commands Processed Команд оброблено Uptime Час роботи Total Keys Усього ключів Hit Ratio Коефіцієнт влучень Server Stats Console day(s) днів Info Інформація Commands Per Second Команд на секунду Ops/s Операцій/с Connected Clients Підключені клієнти Memory Usage Використання пам'яті Mb Mb Network Input Вхідні дані Kb/s Kb/s Network Output Вихідні дані Total Error Replies Error Replies Keys Ключі Auto Refresh Автоматичне оновлення Property Властивість Value Значення Subscribe in Console Переглянути в консолі Slowlog Slowlog Pub/Sub Channels Pub/Sub канали Enable Увімкнути Channel Name Назва каналу Command Команда Processed at Оброблено за Execution Time (μs) Час виконання (мкс) Client Address Адреса клієнта Age (sec) Вік (сек) Idle Незайнятий Flags Прапорці Current Database Поточна база даних Add New Key to Додати новий ключ до Key: Ключ: Type: Тип: Or Import Value from the file (Optional) Any file Select file with value Save Зберегти Edit Connections Group Редагувати групу підключень Add New Connections Group Додати нову групу підключень Group Name: Назва групи: Error Помилка Page Сторінка Enter valid value Введіть дійсне значення Formatting error Помилка форматування Unknown formatter error (Empty response) Невідома помилка форматування (порожня відповідь) [Binary] [Бінарне значення] [Compressed: [Стиснутий: Copy to Clipboard Копіювати в буфер обміну Exit Full Screen Mode Save Changes Зберегти зміни Search string Шукати строку Find Next Знайти далі Find Знайти Regex Регулярний вираз Cannot find more results Не вдається знайти більше результатів Try to decompress: Decompressed: Cannot decompress value using Cannot find any results Не вдається знайти результати Binary value is too large to display Бінарне значення завелике для відображення View as: Переглянути як: Large value (>150kB). Formatters are not available. Велике значення (> 150 кБ). Форматери недоступні. Score Бал Bulk Operations Manager Менеджер масових операцій Invalid RDB path Недійсний шлях до RDB Please specify valid path to RDB file Вкажіть дійсний шлях до файлу RDB Delete keys Видалити ключі Set TTL Встановити TTL Copy keys to another database Скопіювати ключі в іншу базу даних Copy keys Копіювати ключі Import data from rdb file Імпортувати дані з файлу rdb Redis Server: Сервер Redis: Database number: Номер бази даних: Path to RDB file: Шлях до файлу RDB: Path to dump.rdb file Шлях до файлу dump.rdb Select dump.rdb Виберіть dump.rdb Select DB in RDB file: Виберіть БД у файлі RDB: Import keys that match <b>regex</b>: Імпортувати ключі, що відповідають <b>регулярному виразу</b>: Key pattern: Шаблон ключів: Destination Redis Server: Цільовий Redis Server: Destination Redis Server Database Index: Індекс цільової бази даних сервера Redis: Show matched keys Показати ключі, які будуть змінені Show Affected keys Показати ключі, які будуть змінені Matched keys: Знайдені ключі: Affected keys: Ключі, що зазнають впливу: Bulk Operation finished. Масова операція закінчена. Bulk Operation finished with errors Масова операція виконана з помилками Processed: Оброблено: Getting list of affected keys... Отримання списку ключів які будуть змінені... Success Confirmation Підтвердження Do you really want to perform bulk operation? Ви дійсно хочете виконати масову операцію? ID ID Value (represented as JSON object) Значення (представлене як об'єкт JSON) The row has been changed on server.Reload and try again. Рядок змінено на сервері. Перезавантажте та повторіть спробу. Failed to perform actions on %1 keys. Не вдалося виконати дії для %1 ключів. Cannot copy key Не вдається скопіювати ключ Source connection error Помилка підключення до серверу джерела Target connection error Помилка цільового підключення Cannot remove key Не вдається видалити ключ Cannot execute command Не вдається виконати команду Invalid regexp for keys filter. Cannot get the list of affected keys Cannot set TTL for key Не вдається встановити TTL для ключа Your redis-server doesn't support <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a> commands. Ваш redis сервер не підтримує команду <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a>. Key was added. Do you want to reload keys in selected namespace? Ключ додано. Хочете перезавантажити ключі в обраному неймспейсі? Select File Вибрати файл Save to File Зберегти у файл Save Value Зберегти значення Save value to file Зберегти значення у файл Save Raw Value to File Зберегти необроблене значення у файл Save Formatted Value to File Зберегти відформатоване значення у файл Save Raw Value Зберегти необроблене значення Save Formatted Value Зберегти відформатоване значення Save raw value to file Зберегти необроблене значення у файл Save formatted value to file Зберегти відформатоване значення у файл Value was saved to file: Значення було збережено у файл: Cannot connect to redis-server Не вдається підключитися до redis-server Edit Connection Group Редагувати групу підключень Delete Connection Group Видалити групу підключень Do you really want to delete group <b>with all connections</b>? Ви дійсно хочете видалити групу <b>з усіма підключеннями</b>? Order of elements: Порядок елементів: Default За замовчуванням Reverse Зворотний Start date should be less than End date Дата початку має бути меншою за дату завершення Apply filter Застосувати фільтр Network is not accessible. Please ensure that you have internet access and try again. Мережа недоступна. Переконайтесь, що у вас є доступ до Інтернету, і повторіть спробу. Invalid login or password Недійсний логін або пароль Too many requests from your IP Забагато запитів з вашого IP Unknown error. Status code %1 Невідома помилка. Код відповіді %1 Cannot parse server reply Не вдається розібрати відповідь сервера Cannot validate token Не вдається перевірити токен Cannot login - %1. <br/> Please try again or contact <a href='mailto:support@resp.app'>support@resp.app</a> Не вдається ввійти -%1. <br/> Спробуйте ще раз або зв’яжіться з <a href='mailto:support@resp.app'>support@resp.app</a> Expired activation code Invalid activation code Cannot save the update. Disk is full or download folder is not writable. Не вдається зберегти оновлення. Диск заповнений або папка для завантаження недоступна для запису. Download was canceled Завантаження скасовано Network error Помилка мережі Trial is active till Пробна версія активна до Licensed to Ліцензія зареєстрована на Subscription is active until: Підписка активна до: Manage Subscription Керувати підпискою <span style="font-size: 11px;">Powered by awesome <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">open-source software</a> and <a href="http://icons8.com/">icons8</a>.</span> <span style="font-size: 11px;">Використовується крутезне <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">програмне забезпечення з відкритим кодом</a> та <a href="http://icons8.com/">icons8</a>.</span> Getting Started Початок роботи Thank you for choosing RDM. Let's make your Redis experience better. Дякуємо, що обрали RDM. Давайте зробимо вашу роботу з Redis краще. Thank you for choosing RESP.app. Let's make your Redis experience better. Connect to Redis-Server Підключитися до Redis-Server Read the Docs Читати документацію Load more keys SSH Passphrase Unknown Passphrase Continue Yes No Select Unsupported Redis Data type Cannot delete key: ================================================ FILE: src/resources/translations/rdm_zh_CN.ts ================================================ QObject Cannot connect to cluster node %1:%2 无法连接集群节点 %1:%2 Cannot flush db (%1): %2 无法刷新库 (%1): %2 RESP Settings directory is not writable 设置保存文件夹没有写入权限 RESP.app can't save connections file to settings directory. Please change file permissions or restart RESP.app as administrator. RESP.app 无法将连接文件保存到设置目录。 请更改文件权限或以管理员身份重新启动 RESP.app。 Cannot rename key %1: %2 无法重命名键 %1: %2 Cannot persist key '%1'. <br> Key does not exist or does not have an assigned TTL value 无法持久化键 '%1',<br> 键不存在或没有设置TTL时长 Cannot parse scan response 无法解析扫描结果 Server returned unexpected response: 服务器返回了意外结果: Cannot set TTL for key %1 无法给键 %1 设置 TTL Cannot load rows for key %1: %2 无法加载键内容 %1: %2 Invalid row 无效行 Value with the same key already exists 同名键值已经存在 Connection error: 连接错误: Data was loaded from server partially. 部分数据已经从服务器加载。 Cannot load key %1, connection error occurred: %2 无法加载键 %1,连接发生错误:%2 Cannot load key %1 because it doesn't exist in database. Please reload connection tree and try again. 无法加载键 %1,数据库中不存在该键,请重载连接树后重试。 Cannot load TTL for key %1, connection error occurred: %2 无法加载键 %1 的 TTL 值,连接发生错误: %2 Cannot retrieve type of the key: 无法重设键类型: Cannot open file with key value 无法用键值打开文件 Unsupported Redis Data type %1 数据格式不支持 %1 Cannot connect to server '%1'. Check log for details. 无法连接到服务器 '%1',详情请查看日志。 Open Source version of RESP.app <b>doesn't support SSH tunneling</b>.<br /><br /> To get fully-featured application, please buy subscription on <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Every single subscription gives us funds to continue the development process and provide support to our users. <br />If you have any questions please feel free to contact us at <a href='mailto:support@resp.app'>support@resp.app</a> or join <a href='https://t.me/RedisDesktopManager'>Telegram chat</a>. RESP.app 的开源版本<b>不支持 SSH 隧道</b>。<br /><br /> 要获得功能齐全的应用程序,请在 <a href='https://resp 上购买订阅 .app/subscriptions'>resp.app</a>。 <br/><br />每一次订阅都为我们提供了资金来继续开发过程并为我们的用户提供支持。 <br />如果您有任何问题,请随时通过 <a href='mailto:support@resp.app'>support@resp.app</a> 与我们联系或加入 <a href='https:// t.me/RedisDesktopManager'>Telegram chat</a>。 Cannot load keys: %1 无法加载键:%1 Delete key error: %1 删除键失败: %1 Cannot determine amount of used memory by key: %1 无法调用该键占用的内存: %1 Cannot flush database: 清空库错误: Invalid Connection. Check connection settings. 无效连接,请检查连接设置。 Live update was disabled due to exceeded keys limit. Please specify filter more carefully or change limit in settings. 由于超出加载键数量限制,实时更新功能已经关闭。请设置更精确的筛查条件或更改加载限制设定。 Key was added. Do you want to reload keys in selected database? 键已经添加。需要重新加载该数据库的键名吗? Key was added 键已经插入 Another operation is currently in progress 另一个操作正在进行中 Please wait until another operation will be finished. 请耐心等待另一个操作完成。 Do you really want to remove all keys from this database? 确定要删除该数据库里面所有的键吗? Cannot load databases: 无法加载数据库: Live update was disabled 实时更新已关闭 Rename key 重命名键 New name: 新名称: Total pages: 总页数: Size: 大小: TTL: TTL: Set key TTL 设置键的 TTL New TTL: 新的 TTL: Delete 删除 Delete key 删除键 Changes are not saved 更改未保存 Do you want to close key tab without saving changes? 不保存更改关闭标签页吗? Persist key 持久化键 Do you really want to delete this key? 确定要删除该键? Reload Value 重载键值 Add Row 插入行 Add Element to HLL 添加元素到HLL Add 添加 Delete row 删除行 The row is the last one in the key. After removing it key will be deleted. 此行数据是该键最后一行数据。删除此行数据,该键将会被删除。 Do you really want to remove this row? 确定要删除该行数据吗? Search on page... 页面搜索中... Full Search 全文搜索 Value and Console tabs related to this connection will be closed. Do you want to continue? 所有与该连接相关的键值对话框和命令操作对话框都将被关闭,确定要继续吗? Do you really want to delete connection? 确定要删除连接? Connected to cluster. 已连接到集群。 Connected. 已连接。 Switch to %1 mode. Close console tab to stop listen for messages. 切换到推送/订阅模式,关闭标签页来停止接收信息。 Subscribe error: %1 订阅错误:%1 Server %0 服务器 %0 Can't find formatter with name: %1 找不到格式化配置名:%1 Can't find formatter: %1 Invalid callback 无效回调 Can't load list of available formatters from extension server: %1 Can't encode value: %1 Cannot decode value using %1 formatter. 无法使用 %1 格式化配置来解析值。 Cannot validate value using %1 formatter. 无法使用 %1 格式化配置来效验值。 Cannot encode value using %1 formatter. 无法使用 %1 编码键值 Loading key: %1 from db %2 从 db %2 加载 key: %1 Cannot open value tab 无法打开键值对话框 Connection error 连接错误 Connection error. Can't open value tab. 连接错误,无法打开键值对话框。 Cannot reload key value: %1 无法重载键值: %1 Cannot load key value: %1 无法加载键值:%1 Connect to Redis Server 连接到 Redis 服务器 Import 导入 Import Connections 导入连接 Export 导出 Export Connections 导出连接 Report issue 报告错误 Documentation 文档 Join Telegram Chat 加入 Telegram 聊天组 Follow 关注 Star on GitHub! 给我们的GitHub加个星星吧! Log 日志 Extension Server Settings 设置 New Connection Settings 新连接设置 How to connect 怎么连接 Connection Settings 连接设置 Create connection from Redis URL 从Redis URL创建连接 Learn more about Redis URL: 认识Redis URL: Connection guides 连接向导 Local or Public Redis 本地或对外的Redis Redis with SSL/TLS 使用SSL/TLS的Redis SSH tunnel SSH通道 UNIX socket UNIX套接字 Cannot figure out how to connect to your redis-server? 不知道怎么连接到您的Redis服务端吗? <a href="https://docs.resp.app/en/latest/quick-start/">Read the Docs</a>, <a href="mailto:support@resp.app">Contact Support</a> or ask for help in our <a href="https://t.me/RedisDesktopManager">Telegram Group</a> <a href="https://docs.resp.app/en/latest/quick-start/">查看文档</a>, <a href="mailto:support@resp.app">联系支持</a> 或者点这里寻求帮助 <a href="https://t.me/RedisDesktopManager">Telegram Group</a> Don't have running Redis? Redis没有启动吗? Spin up hassle-free Redis on Digital Ocean 快速使用Digital Ocean上的Redis Skip 跳过 Name: 名字: Connection Name 连接名 Address: 地址 redis-server host Redis 服务器地址 (Optional) redis-server authentication password (可选) Redis 服务器验证密码 Security 安全 Public Key: 公钥: (Optional) Public Key in PEM format (可选) PEM 格式公钥 Select public key in PEM format 选择 PEM 格式公钥 (Optional) Private Key in PEM format (可选) PEM 格式私钥 Select private key in PEM format 选择 PEM 格式私钥 Authority: 授权: (Optional) Authority in PEM format (可选) PEM 格式授权 Select authority file in PEM format 选择 PEM 格式授权文件 SSH Tunnel SSH 通道 SSH Address: SSH 地址: Remote Host with SSH server SSH 远程服务器 SSH User: SSH 用户: Valid SSH User Name 验证 SSH 用户名 Private Key 私钥 Path to Private Key in PEM format PEM 格式私钥路径 <b>Tip:</b> Use <code>⌘ + Shift + .</code> to show hidden files and folders in dialog <b>提示:</b> 使用 <code>⌘ + Shift + .</code> 在对话框中显示隐藏文件和文件夹 Password 密码 SSH User Password SSH 用户密码 Enable TLS-over-SSH (<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) 启用 TLS-over-SSH (<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) Advanced Settings 高级设置 For better network performance please use 127.0.0.1 请使用127.0.0.1获得更好的网络连接速度 Username: 用户名: (Optional) redis-server authentication username (Redis >6.0) 可选:服务端认证用户名 (Redis >6.0) SSL / TLS SSL / TLS Enable strict mode: 打开严格模式: Use SSH Agent (Optional) Custom SSH Agent Path Select SSH Agent Additional configuration is required to enable SSH Agent support Passphrase for provided private key 提供的私钥的密码 Password request will be prompt prior to connection 将会在连接前询问密码 Ask for password 询问密码 Keys loading 键加载 Default filter: 默认过滤: Pattern which defines loaded keys from redis-server 指定加载键名表达式: Namespace Separator: 命名空间分隔符: Separator used for namespace extraction from keys 键名中命名空间分隔符 Timeouts & Limits 设置超时和限制 Connection Timeout (sec): 连接超时 (秒): Execution Timeout (sec): 执行超时 (秒): Databases discovery limit: 数据库发现限制: Cluster 集群 Change host on cluster redirects: 修改集群重定向: Formatters 格式化程序 Default value formatter: 默认值格式化程序: Auto detect (JSON / Plain Text / HEX) 自动检测(JSON / 纯文本 / HEX) Last selected 最后选择 Select formatter ... 选择格式化程序... Appearance Icon color: Invalid settings detected! 检测到无效的设置! Test Connection 测试连接 OK 确定 Cancel 取消 General 通用 Application will be restarted to apply these settings. 重启软件来启用新的设置。 Language 语言 Application will be restarted to apply this setting. 新设置将在软件重启后生效 Font 字体 Font Size 字体大小 Dark Mode 暗色模式 Maximum Formatted Value Size 最大格式化长度 Size in bytes 字节长度 Use system proxy settings 使用系统代理设置 Use system proxy only for HTTP(S) requests 只对HTTP(S)请求使用系统代理设置 Value Editor 内容编辑器 Maximum amount of items per page Connections Tree 连接树 Show namespaced keys on top 在头部展示命名空间键名 Reopen namespaces on reload 重载时重新打开命名空间 (Disable to improve treeview performance) (禁用树状视图提高性能) Show only last part for namespaced keys 仅显示命名空间中键名最后一部分内容 Limit for SCAN command Maximum amount of rendered child items 子项目的最大渲染数量 Live update maximum allowed keys 实时更新最大允许键数量 Live update interval (in seconds) 实时更新间隔 (秒) External Value View Formatters 外部键值格式化配置 Formatters path: %0 格式化配置路径:%0 Server Url: Basic Auth: User Response timeout (in seconds) Available Data Formatters Reload Id Name 名称 Read Only Version 版本 Quick Start Guide 快速入门指南 Successful connection to redis-server 连接 Redis 服务器成功 Can't connect to redis-server 无法连接 Redis 服务器 Add Group 添加组 Regroup connections 重组连接 Exit Regroup Mode 退出重组模式 Bulk Operations Manager 批量操作管理 Invalid RDB path 无效的RDB路径 Please specify valid path to RDB file 请指定有效的RDB文件路径 Delete keys 删除键 Set TTL 设置TTL Copy keys to another database 复制键到其他库 Copy keys 复制键 Import data from rdb file 从RDB文件导入数据 Redis Server: Redis 服务器: Database number: 数据库编号: Path to RDB file: RDB文件路径: Select DB in RDB file: 从RDB文件选择库: Key pattern: 键名表达式: Import keys that match <b>regex</b>: 导入匹配<b>regex</b>的键: Destination Redis Server: 目标 Redis 服务器: Destination Redis Server Database Index: 目标 Redis 数据库编号: Show matched keys 显示匹配的键 Show Affected keys 显示受影响的键 Affected keys: 受影响的键: Matched keys: 匹配的键: Bulk Operation finished. 批量操作完成。 Bulk Operation finished with errors 批量操作完成,但发生了一些错误。 Processed: 已处理: Getting list of affected keys... 获取受影响的键列表... Success 成功 Confirmation 确认 Do you really want to perform bulk operation? 确认要执行批量操作? Sign in with resp.app account 使用 resp.app 账号登陆 Renew your subscription 更新订阅 Your trial has ended. 试用版已结束 You have no active subscription 您没有可用订阅 No internet connection 无网络 Your trial has ended 您的试用已结束 To use this version you need to renew your subscription. 要继续使用该版本,需要更新你的订阅。 If you’re behind a proxy please enable 如果您处于代理之中,请启用代理 option before sign-in. 登陆前选项 Sign in with RESP.app account 使用 RESP.app 帐户登录 Please make sure that RESP.app is not blocked by a firewall and you have an internet connection. 请确保 RESP.app 未被防火墙阻止并且您有互联网连接。 Please purchase a subscription to continue using RESP.app. 请购买订阅以继续使用 RESP.app。 If you have any questions please contact support 遇到任何问题,请联系支持。 Renew Subscription 更新订阅 Buy Subscription 购买订阅 Try Again 重试 Email: 邮箱: Password: 密码: Show password 显示密码 Forgot password? 忘记密码? Sign In 登录 Please enter email & password to sign in. 请输入邮箱和密码来登录。 Offline Activation 离线激活 Paste Activation code here 在这里粘贴激活码 Where can I find my activation code? 我在哪里能找到我的激活码? Activate 激活 Please enter valid activation code. 请输入正确的激活码。 (Removed) (删除) Open Keys Filter 打开键过滤器 Reload Keys in Database 重载该数据库的键 Add New Key 添加新键 Disable Live Update 关闭实时更新 Enable Live Update 打开实时更新 Open Console 打开控制台 Analyze Used Memory 分析内存占用 Bulk Operations 批量操作 Flush Database 清空数据库 Delete keys with filter 使用过滤器删除键 Set TTL for multiple keys 设置多个键的TTL Copy keys from this database to another 从本库复制键到其他库 Import keys from RDB file 从RDB文件导入键 Back 返回 Copy Key Name 复制键值 Reload Namespace 重载命名空间 Copy Namespace Pattern 复制命名空间模式 Delete Namespace 删除命名空间 Disconnect 断开连接 Server Info 服务器信息 Reload Server 重载服务器 Unload All Data 卸载所有数据 Edit Connection Settings 编辑连接设置 Duplicate Connection 复制连接 Delete Connection 删除连接 Connecting... 连接中... Clear 清除 Arguments 参数 Description 描述 Available since 可用自 Close 关闭 View Server Info Redis Version Redis 版本 Used memory 已使用的内存 Cmd Processed Monitor Commands Clients 客户端 Server Actions Commands Processed 已执行的命令 Uptime 运行时间 Total Keys 键总量 Hit Ratio 命中率 Server Stats Console day(s) Info 信息 Commands Per Second 每秒执行命令数 Ops/s 每秒查询率 Connected Clients 已连接的客户端数量 Memory Usage 内存占用 Mb Mb Network Input 网络输入 Kb/s Kb/s Network Output 网络输出 Total Error Replies Error Replies Keys 键数量 Auto Refresh 自动刷新 Property 属性 Value 键值 Subscribe in Console 控制台订阅 Slowlog 慢查询日志 Pub/Sub Channels 推送/订阅 通道 Enable 启用 Channel Name 通道名 Command 指令 Processed at 处理于 Execution Time (μs) 执行时长 (μs) Client Address 客户端地址 Age (sec) 时长 (sec) Idle 空闲 Flags 标记 Current Database 当前库 Add New Key to 添加新键到 Key: 键名: Type: 类型: Or Import Value from the file 或从文件中导入值 (Optional) Any file (可选)任意文件 Select file with value 使用值选择文件 Save 保存 Edit Connections Group 编辑连接组 Add New Connections Group 添加连接组 Group Name: 组名: Error 错误 Page Enter valid value 请输入有效的值 Formatting error 格式化错误 Unknown formatter error (Empty response) 未知的格式化错误(无响应) [Binary] [二进制] [Compressed: [压缩的: Copy to Clipboard 复制到剪切板 Exit 退出 Full Screen Mode 全屏模式 Save Changes 保存更改 Search string 搜索字符串 Find Next 查找下一条 Find 查找 Regex 正则表达式 Cannot find more results 找不到更多的结果 Try to decompress: 尝试解压: Decompressed: 解压: Cannot decompress value using 无法解压以 Cannot find any results 找不到任何结果 Binary value is too large to display 二进制内容太长无法展示 View as: 查看 Large value (>150kB). Formatters are not available. 键值内容过大(>150kB),格式化配置无效。 Score 分数 Path to dump.rdb file 导出为 dump.rdb 文件的路径 Select dump.rdb 选择 dump.rdb ID ID Value (represented as JSON object) 值 (表示为 JSON 对象) The row has been changed on server.Reload and try again. 服务端该行内容已经改变,请重新加载。 Failed to perform actions on %1 keys. 对键 %1 执行操作时失败。 Cannot copy key 无法复制键 Source connection error 源连接错误 Target connection error 目标连接错误 Cannot remove key 无法清除键 Cannot execute command 无法执行命令 Invalid regexp for keys filter. 键筛选表达式无效 Cannot get the list of affected keys 无法获取受到影响的键列表 Cannot set TTL for key 无法给该键设置TTL Your redis-server doesn't support <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a> commands. 你的Redis服务端不支持 <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a> 指令 Key was added. Do you want to reload keys in selected namespace? 键已经添加。需要重新加载选中的命名空间中的键吗? Network is not accessible. Please ensure that you have internet access and try again. 网络无效,请确保已经连上互联网,然后重试。 Invalid login or password 无效的账号或密码 Too many requests from your IP 过多请求来源你的IP Unknown error. Status code %1 未知错误,状态码 %1 Cannot parse server reply 无法解析服务器响应 Cannot validate token 无法效验令牌 Cannot login - %1. <br/> Please try again or contact <a href='mailto:support@resp.app'>support@resp.app</a> 无法登录 - %1。<br/> 请重试。或直接联系 <a href='mailto:support@resp.app'>support@resp.app</a> Expired activation code 激活码已过期 Invalid activation code 无效的激活码 Cannot save the update. Disk is full or download folder is not writable. 无法保存更新文件,磁盘已满或下载目录无法写入。 Download was canceled 下载已取消 Network error 网络错误 Select File 选择文件 Save to File 保存到文件 Save Value 保存内容 Save value to file 保存内容到文件 Save Raw Value to File 保存原始内容到文件 Save Formatted Value to File 保存格式化后的内容到文件 Save Raw Value 保存原始内容 Save Formatted Value 保存格式化后的内容 Save raw value to file 保存原始内容到文件 Save formatted value to file 保存格式化后的内容到文件 Value was saved to file: 内容已保存到文件: Cannot connect to redis-server 无法连接到Redis服务器 Edit Connection Group 编辑连接组 Delete Connection Group 删除连接组 Do you really want to delete group <b>with all connections</b>? 真的要删除组内<b>所有连接</b>吗? Order of elements: 要素排序 Default 默认 Reverse 反向 Start date should be less than End date 开始时间应该早于结束时间 Apply filter 应用筛选条件 Trial is active till 试用到 Licensed to 授权给 Subscription is active until: 订阅到期时间为: Manage Subscription 管理订阅 <span style="font-size: 11px;">Powered by awesome <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">open-source software</a> and <a href="http://icons8.com/">icons8</a>.</span> <span style="font-size: 11px;">Powered by awesome <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">open-source software</a> and <a href="http://icons8.com/">icons8</a>.</span> Getting Started 使用入门 Thank you for choosing RESP.app. Let's make your Redis experience better. 感谢您选择 RESP.app。 让我们给您的 Redis 体验更好。 Connect to Redis-Server 连接到Redis服务器 Read the Docs 阅读文档 Load more keys 加载更多的键 Yes No SSH Passphrase SSH 密钥 Unknown 未知 Passphrase 密钥 Continue 继续 Select Unsupported Redis Data type Cannot delete key: ================================================ FILE: src/resources/translations/rdm_zh_TW.ts ================================================ QObject Cannot connect to cluster node %1:%2 無法連線到叢集節點 %1:%2 Cannot flush db (%1): %2 無法清空資料庫 (%1): %2 RESP Settings directory is not writable 設定儲存資料夾沒有寫入權限 RESP.app can't save connections file to settings directory. Please change file permissions or restart RESP.app as administrator. RESP.app 無法儲存設定檔。請更改檔寫入權限或者以管理員模式啟動 RESP.app。 Cannot rename key %1: %2 無法重新命名鍵 %1: %2 Cannot persist key '%1'. <br> Key does not exist or does not have an assigned TTL value 無法將鍵持久化 '%1' <br> 鍵不存在或是不會逾時 Cannot parse scan response 無法解析 scan 的結果 Server returned unexpected response: 伺服器返回未預期的回應: Cannot set TTL for key %1 無法設定鍵 %1 的 TTL Cannot load rows for key %1: %2 無法載入鍵 %1 的資料: %2 Invalid row 無效資料 Value with the same key already exists 已經存在同名的鍵 Connection error: 連線錯誤: Data was loaded from server partially. 部分資料已經從伺服器載入。 Cannot load key %1, connection error occurred: %2 無法載入鍵 %1,連線發生錯誤: %2 Cannot load key %1 because it doesn't exist in database. Please reload connection tree and try again. 無法載入鍵 %1,資料庫中不存在該鍵,請重新載入連線樹後重試。 Cannot load TTL for key %1, connection error occurred: %2 無法載入鍵 %1 的 TTL 值,連線發生錯誤: %2 Cannot retrieve type of the key: 無法取得鍵的類型: Cannot open file with key value 無法以鍵值開啟檔案 Unsupported Redis Data type %1 不支援的 Redis 資料類型 %1 Cannot connect to server '%1'. Check log for details. 無法連線到伺服器 '%1' 。細節請查看紀錄檔。 Open Source version of RESP.app <b>doesn't support SSH tunneling</b>.<br /><br /> To get fully-featured application, please buy subscription on <a href='https://resp.app/subscriptions'>resp.app</a>. <br/><br />Every single subscription gives us funds to continue the development process and provide support to our users. <br />If you have any questions please feel free to contact us at <a href='mailto:support@resp.app'>support@resp.app</a> or join <a href='https://t.me/RedisDesktopManager'>Telegram chat</a>. 開源版本的 RESP.app <b>不支援 SSH 隧道功能</b>。<br /><br />若要取得完整功能的程式,請在 <a href='https://resp.app/subscriptions'>resp.app</a> 上購買訂閱。<br/><br />每個訂閱都是我們繼續開發以及支援使用者的原動力。<br />如果你有任何問題,請聯絡 <a href='mailto:support@resp.app'>support@resp.app</a> 或是加入 <a href='https://t.me/RedisDesktopManager'>Telegram 聊天群組</a>。 Cannot load keys: %1 無法載入鍵: %1 Delete key error: %1 刪除鍵時發生錯誤: Cannot determine amount of used memory by key: %1 無法判定鍵所消耗的記憶體: %1 Cannot flush database: 無法清空資料庫: Invalid Connection. Check connection settings. 無效連線,請檢查連線設定。 Live update was disabled due to exceeded keys limit. Please specify filter more carefully or change limit in settings. 由於超出載入鍵的數量限制,同步更新功能已經關閉。請設定更精確的篩選條件或更改載入限制設定。 Key was added. Do you want to reload keys in selected database? 已經添加鍵。需要重新載入該資料庫的鍵名嗎? Key was added 已經插入鍵 Another operation is currently in progress 另一項操作正在進行中 Please wait until another operation will be finished. 請耐心等待另一項操作完成。 Do you really want to remove all keys from this database? 確定要刪除該資料庫裡面所有的鍵嗎? Cannot load databases: 無法載入資料庫: Live update was disabled 同步更新已經禁止 Rename key 重新命名鍵 New name: 新名稱: Total pages: 總頁數: Size: 大小: TTL: TTL: Set key TTL 設定鍵的 TTL New TTL: 新的 TTL: Delete 刪除 Delete key 刪除鍵 Changes are not saved 並未儲存變更 Do you want to close key tab without saving changes? 要不儲存變更就關閉頁籤嗎? Persist key 將鍵持久化 Do you really want to delete this key? 確定要刪除該鍵? Reload Value 重新載入鍵值 Add Row 插入列 Add Element to HLL 新增元素到 HHL Add 新增 Delete row 刪除列 The row is the last one in the key. After removing it key will be deleted. 此列資料是該鍵最後一列。刪除此列資料,該鍵將會被刪除。 Do you really want to remove this row? 確定要刪除此列資料嗎? Search on page... 頁面搜尋... Full Search 全文搜尋 Value and Console tabs related to this connection will be closed. Do you want to continue? 所有與該連線相關的鍵值對話方塊和指令操作對話方塊都將被關閉,確定要繼續嗎? Do you really want to delete connection? 確定要刪除連線? Connected to cluster. 已連線到叢集伺服器。 Connected. 已連線。 Switch to %1 mode. Close console tab to stop listen for messages. 切換為 %1 模式。關閉頁籤以停止監聽訊息。 Switch to Pub/Sub mode. Close console tab to stop listen for messages. 切斷到 發布/訂閱 模式。關閉控制台以停止監聽訊息。 Subscribe error: %1 訂閱錯誤: %1 Server %0 伺服器 %0 Can't find formatter with name: %1 找不到格式化工具: %1 Can't find formatter: %1 Invalid callback 無效回調 Can't load list of available formatters from extension server: %1 Can't encode value: %1 Cannot decode value using %1 formatter. 無法使用格式化工具解碼值 %1 Cannot validate value using %1 formatter. 無法使用格式化工具驗證值 %1 Cannot encode value using %1 formatter. 無法使用格式化工具編碼值 %1 Loading key: %1 from db %2 從資料庫 %2 中載入鍵 %1 Cannot open value tab 無法打開鍵值對話方塊 Connection error 連接錯誤 Connection error. Can't open value tab. 連線錯誤,無法打開鍵值對話方塊。 Cannot reload key value: %1 無法重新載入鍵值: %1 Cannot load key value: %1 無法載入鍵值: %1 Connect to Redis Server 連線到 Redis 伺服器 Import 匯入 Import Connections 匯入連線 Export 匯出 Export Connections 匯出連線 Report issue 回報問題 Documentation 說明文件 Join Telegram Chat 加入 Telegram 聊天群組 Follow 追隨 Star on GitHub! 在 GitHub 上給個 Star Log 紀錄 Extension Server Settings 設定 New Connection Settings 新連線設定 How to connect 如何連線 Connection Settings 連線設定 Create connection from Redis URL 以 Redis URL 建立連線 Learn more about Redis URL: 了解更多關於 Redis URL: Connection guides 連線嚮導 Local or Public Redis 本機或公開的 Redis Redis with SSL/TLS 使用 SSL/TLS 的 Redis SSH tunnel SSH 隧道 UNIX socket UNIX socket Cannot figure out how to connect to your redis-server? 不知道如何連線到您的 Redis 伺服器嗎? <a href="https://docs.resp.app/en/latest/quick-start/">Read the Docs</a>, <a href="mailto:support@resp.app">Contact Support</a> or ask for help in our <a href="https://t.me/RedisDesktopManager">Telegram Group</a> <a href="https://docs.resp.app/en/latest/quick-start/">閱讀文件</a>,<a href="mailto:support@resp.app">聯絡客服</a>或是在 <a href="https://t.me/RedisDesktopManager">Telegram 群組</a> 內請求協助。 Don't have running Redis? Redis 沒有在執行中嗎? Spin up hassle-free Redis on Digital Ocean 快速使用 Digital Ocean 上的 Redis Skip 略過 Name: 名稱: Connection Name 連線名稱 Address: 位址: redis-server host Redis 伺服器位址 (Optional) redis-server authentication password (可選) Redis 伺服器的認證密碼 Security 安全設定 Public Key: 公鑰: (Optional) Public Key in PEM format (可選) PEM 格式的公鑰 Select public key in PEM format 選擇 PEM 格式的公鑰 (Optional) Private Key in PEM format (可選) PEM 格式的私鑰 Select private key in PEM format 選擇 PEM 格式的私鑰 Authority: 授權證書: (Optional) Authority in PEM format (可選) PEM 格式的授權證書 Select authority file in PEM format 選擇 PEM 格式的授權證書 SSH Tunnel SSH 隧道 SSH Address: SSH 位址: Remote Host with SSH server SSH 遠端伺服器位址 SSH User: SSH 使用者: Valid SSH User Name 有效的 SSH 使用者名稱 Private Key 私鑰 Path to Private Key in PEM format PEM 格式私鑰路徑 <b>Tip:</b> Use <code>⌘ + Shift + .</code> to show hidden files and folders in dialog <b>提示: </b> <code>⌘ + Shift + .</code> 可以顯示隱藏的檔案與資料夾 Password 密碼 SSH User Password SSH 使用者密碼 Enable TLS-over-SSH (<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) 啟用 TLS-over-SSH (<b>AWS ElastiCache</b> <b>Encryption in-transit</b>) Advanced Settings 進階設定 For better network performance please use 127.0.0.1 使用 127.0.0.1 以提高網路性能 Username: 使用者名稱: (Optional) redis-server authentication username (Redis >6.0) (可選) Redis 伺服器認證使用者名稱 (Redis >6.0) SSL / TLS SSL / TLS Enable strict mode: 啟用嚴格模式: Use SSH Agent (Optional) Custom SSH Agent Path Select SSH Agent Additional configuration is required to enable SSH Agent support Passphrase for provided private key 私鑰的密詞 Password request will be prompt prior to connection 將會在連接前詢問密碼 Ask for password 詢問密碼 Keys loading 鍵的載入 Default filter: 預設篩選器: Pattern which defines loaded keys from redis-server 指定載入鍵名運算式: Namespace Separator: 命名空間的分隔符號: Separator used for namespace extraction from keys 從鍵名中提取命名空間用的分隔符號 Timeouts & Limits 超時 & 限制 Connection Timeout (sec): 連線逾時 (秒): Execution Timeout (sec): 執行超時 (秒): Databases discovery limit: 資料庫探索上限: Cluster 叢集 Change host on cluster redirects: 在叢集重定向後改變 host : Formatters Default value formatter: Auto detect (JSON / Plain Text / HEX) Last selected Select formatter ... Appearance Icon color: Invalid settings detected! 檢測到無效的設定! Test Connection 測試連線 OK 確定 Cancel 取消 General 一般 Application will be restarted to apply these settings. 程式將會重新啟動以套用此設定 Language 語言 Application will be restarted to apply this setting. 程式將會重新啟動以套用新的設定 Font 字體 Font Size 字體大小 Dark Mode 深色模式 Maximum Formatted Value Size 最大格式化長度 Size in bytes 長度(位元組) Use system proxy settings 使用系統的代理設定 Use system proxy only for HTTP(S) requests 只為 HTTP(S) 使用系統的代理 Value Editor 值編輯器 Maximum amount of items per page Connections Tree 連線列表 Show namespaced keys on top 置頂有命名空間的鍵 Reopen namespaces on reload 重新載入時重新打開命名空間 (Disable to improve treeview performance) (停用樹狀檢視以提高性能) Show only last part for namespaced keys 對有命名空間的鍵只顯示最後一部分 Limit for SCAN command Maximum amount of rendered child items 子項目的最大渲染數量 Live update maximum allowed keys 同步更新最大允許鍵數量 Live update interval (in seconds) 同步更新時間 (秒) External Value View Formatters 外部的值格式化工具 Formatters path: %0 格式化工具路徑: %0 Server Url: Basic Auth: User Response timeout (in seconds) Available Data Formatters Reload Id Name 名稱 Read Only Version 版本 Quick Start Guide 快速入門指南 Successful connection to redis-server 成功連線到 Redis 伺服器 Can't connect to redis-server 無法連線到 Redis 伺服器 Add Group 新增分組 Regroup connections 重組連線 Exit Regroup Mode 離開分組模式 Bulk Operations Manager 批次操作管理器 Invalid RDB path 無效的 RDB 路徑 Please specify valid path to RDB file 請指定有效的 RDB 檔案 Delete keys 刪除鍵 Set TTL 設定 TTL Copy keys to another database 複製鍵到其他資料庫 Copy keys 複製鍵 Import data from rdb file 從 RDB 檔案中匯入資料 Redis Server: Redis 伺服器: Database number: 資料庫編號: Path to RDB file: RDB 檔案的路徑: Select DB in RDB file: 選擇 RDB 檔案中的資料庫: Key pattern: 鍵名運算式: Import keys that match <b>regex</b>: 匯入符合<b>正規表達式</b>的鍵: Destination Redis Server: 目標 Redis 伺服器: Destination Redis Server Database Index: 目標資料庫編號: Show matched keys 顯示符合的鍵 Show Affected keys 顯示受影響的鍵 Affected keys: 受影響的鍵: Matched keys: 符合的鍵: Bulk Operation finished. 批次操作完成。 Bulk Operation finished with errors 批次操作完成但途中曾發生錯誤 Processed: 已處理: Getting list of affected keys... 正在取得受影響的鍵的清單... Success 成功 Confirmation 確認 Do you really want to perform bulk operation? 確認要執行批次操作? Sign in with resp.app account 以 resp.app 的帳號登入 Renew your subscription 續期您的訂閱 Your trial has ended. 您的試用已經到期 You have no active subscription 您沒有可用的訂閱 No internet connection 無網絡連線 Your trial has ended 您的試用已結束 To use this version you need to renew your subscription. 您必須續期訂閱已繼續使用此版本。 If you’re behind a proxy please enable 如果您處於代理之中,請啟用 option before sign-in. 選項(在登入前)。 Sign in with RESP.app account 以 resp.app 的帳號登入 Please make sure that RESP.app is not blocked by a firewall and you have an internet connection. 請確保 RESP.app 沒有被防火牆阻擋,並且網絡連線正常。 Please purchase a subscription to continue using RESP.app. 請購買訂閱以繼續使用 RESP.app 。 If you have any questions please contact support 如果您有任何問題,請聯絡客服 Renew Subscription 續期訂閱 Buy Subscription 購買訂閱 Try Again 重試 Email: Email: Password: 密碼: Show password 顯示密碼 Forgot password? 忘記密碼? Sign In 登入 Please enter email & password to sign in. 請輸入 email 與密碼登入。 Offline Activation 離線啟用 Paste Activation code here 在此處貼上啟用碼 Where can I find my activation code? 我能在哪裡找到我的啟用碼? Activate 啟用 Please enter valid activation code. 請輸入啟用碼 (Removed) (已移除) Open Keys Filter 打開鍵篩選器 Reload Keys in Database 重新載入資料庫中的鍵 Add New Key 新增鍵 Disable Live Update 停用同步更新 Enable Live Update 啟用同步更新 Open Console 打開控制台 Analyze Used Memory 分析記憶體用量 Bulk Operations 批次操作 Flush Database 清空資料庫 Delete keys with filter 使用篩選器來刪除鍵 Set TTL for multiple keys 為多個鍵設定 TTL Copy keys from this database to another 從此資料庫中複製鍵到另一個資料庫 Import keys from RDB file 從 RDB 檔案中匯入鍵 Back 返回 Copy Key Name 複製鍵 Reload Namespace 重新載入命名空間 Copy Namespace Pattern 複製命名空間運算式 Delete Namespace 刪除命名空間 Disconnect 中斷連線 Server Info 伺服器資訊 Reload Server 重新載入伺服器 Unload All Data 卸載所有資料 Edit Connection Settings 編輯連線設定 Duplicate Connection 複製連線 Delete Connection 刪除連線 Connecting... 連線中... Clear 清除 Arguments 參數 Description 描述 Available since 可用自 Close 關閉 View Server Info Redis Version Redis 版本 Used memory 已使用記憶體 Cmd Processed Monitor Commands Clients 連線數 Server Actions Commands Processed 已執行指令 Uptime 上線時間 Total Keys 鍵總數 Hit Ratio 命中率 Server Stats Console day(s) Info 資訊 Commands Per Second 每秒指令數 Ops/s 操作/秒 Connected Clients 已連線的客戶端 Memory Usage 記憶體佔用 Mb Mb Network Input 網路輸入 Kb/s Kb/s Network Output 網路輸出 Total Error Replies Error Replies Keys 鍵數量 Auto Refresh 自動重整 Property 屬性 Value Subscribe in Console 在控制台中訂閱 Slowlog 慢紀錄 Pub/Sub Channels 發布/訂閱 頻道 Enable 啟用 Channel Name 頻道名稱 Command 指令 Processed at 已處理於 Execution Time (μs) 執行時間 (微秒) Client Address 客戶端位址 Age (sec) 連線時長 (秒) Idle 閒置 Flags 旗標 Current Database 當前資料庫 Add New Key to 新增鍵到 Key: 鍵: Type: 類型: Or Import Value from the file 或從檔案匯入值 (Optional) Any file (可選) 任何檔案 Select file with value 以值選擇檔案 Save 儲存 Edit Connections Group 編輯連線的群組 Add New Connections Group 新增連線群組 Group Name: 群組名稱: Error 錯誤 Page Enter valid value 請輸入有效的值 Formatting error 格式化錯誤 Unknown formatter error (Empty response) 未知格式化錯誤 (沒有回應) [Binary] [二進位制內容] [Compressed: [被壓縮的: Copy to Clipboard 複製到剪貼簿 Exit Full Screen Mode Save Changes 儲存變更 Search string 搜尋字串 Find Next 尋找下一筆 Find 尋找 Regex 正規表示式 Cannot find more results 找不到更多結果 Try to decompress: 嘗試解壓縮: Decompressed: 解壓縮: Cannot decompress value using 無法解壓縮以 Cannot find any results 找不到任何結果 Binary value is too large to display 二進位制內容過長而無法顯示 View as: 以...開啟: Large value (>150kB). Formatters are not available. 內容過大 (>150kB) 無法格式化。 Score 分數 Path to dump.rdb file dump.rdb 的路徑 Select dump.rdb 選擇 dump.rdb ID ID Value (represented as JSON object) 值 (以 JSON 物件表示) The row has been changed on server.Reload and try again. 此列資料已在伺服器上被修改,請重新載入後再試一次。 Failed to perform actions on %1 keys. 無法在鍵 %1 上執行動作。 Cannot copy key 無法複製鍵 Source connection error 來源連線錯誤 Target connection error 目標連線錯誤 Cannot remove key 無法刪除鍵 Cannot execute command 無法執行指令 Invalid regexp for keys filter. 鍵篩選器的正規表達式無效 Cannot get the list of affected keys 無法取得受影響的鍵的清單 Cannot set TTL for key 無法設定 TTL 給鍵 Your redis-server doesn't support <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a> commands. 你的 Redis 伺服器不支援 <a href='https://redis.io/commands/memory-usage'><b>MEMORY</b></a> 指令。 Key was added. Do you want to reload keys in selected namespace? 已新增鍵。你想要重新載入命名空間中的鍵嗎? Network is not accessible. Please ensure that you have internet access and try again. 無法存取網路。請確認您可以存取網路後重新再試。 Invalid login or password 無效的登入資料 Too many requests from your IP 您的 IP 發起過多的請求 Unknown error. Status code %1 未知錯誤。狀態碼 %1 Cannot parse server reply 無法解析伺服器回應 Cannot validate token 無法驗證權杖 Cannot login - %1. <br/> Please try again or contact <a href='mailto:support@resp.app'>support@resp.app</a> 無法登入 - %1 。<br/> 請再試一次或聯絡 <a href='mailto:support@resp.app'>support@resp.app</a> Expired activation code 過期的啟動碼 Invalid activation code 無效的啟動碼 Cannot save the update. Disk is full or download folder is not writable. 無法保存更新檔,可能是硬碟已滿或是資料夾無法寫入。 Download was canceled 下載已被取消 Network error 網路錯誤 Select File 選擇檔案 Save value to file 儲存值到檔案 Save Raw Value to File 儲存原始值到檔案 Save Formatted Value to File 儲存格式化的值到檔案 Save Raw Value 儲存原始值 Save Formatted Value 儲存格式化的值 Save raw value to file 儲存原始值到檔案 Save formatted value to file 儲存格式化的值到檔案 Value was saved to file: 已儲存值到檔案: Cannot connect to redis-server 無法連線到 Redis 伺服器 Edit Connection Group 編輯連線群組 Delete Connection Group 刪除連線群組 Do you really want to delete group <b>with all connections</b>? 您真的要刪除群組<b>以及其中的連線</b>嗎? Order of elements: 元素排序: Default 預設 Reverse 倒序 Start date should be less than End date 起始日期必須早於終止日期 Apply filter 套用篩選器 Trial is active till 試用到 Licensed to 授權給 Subscription is active until: 訂閱到: Manage Subscription 管理訂閱 <span style="font-size: 11px;">Powered by awesome <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">open-source software</a> and <a href="http://icons8.com/">icons8</a>.</span> <span style="font-size: 11px;">由卓越的 <a href="https://github.com/uglide/RedisDesktopManager/tree/2021/3rdparty">開源軟體</a> 以及 <a href="http://icons8.com/">icons8</a> 驅動。</span> Getting Started 入門指南 Thank you for choosing RESP.app. Let's make your Redis experience better. 感謝您選用 RESP.app 。我們一起讓 Redis 有更好的使用體驗吧! Connect to Redis-Server 連線到 Redis 伺服器 Read the Docs 閱讀文件 Load more keys 載入更多鍵 Yes No SSH Passphrase SSH 密詞 Unknown 未知 Passphrase 密碼 Continue 繼續 Select Unsupported Redis Data type Cannot delete key: ================================================ FILE: src/resp.pro ================================================ #------------------------------------------------- # # RESP.app (formerly Redis Desktop Manager) # #------------------------------------------------- CCACHE_BIN = $$system(which ccache) !isEmpty(CCACHE_BIN) { load(ccache) CONFIG+=ccache } QT += core gui network concurrent widgets quick quickwidgets charts svg TARGET = resp TEMPLATE = app !defined(VERSION, var) { VERSION=2022.0.0-dev } message($$VERSION) DEFINES += APP_VERSION=\\\"$$VERSION\\\" SOURCES += \ $$PWD/main.cpp \ $$PWD/app/app.cpp \ $$PWD/app/events.cpp \ $$PWD/app/qmlutils.cpp \ $$PWD/app/jsonutils.cpp \ $$PWD/app/qcompress.cpp \ $$files($$PWD/app/models/*.cpp) \ $$files($$PWD/app/models/key-models/*.cpp) \ $$files($$PWD/modules/connections-tree/*.cpp) \ $$files($$PWD/modules/connections-tree/items/*.cpp) \ $$files($$PWD/modules/console/*.cpp) \ $$files($$PWD/modules/value-editor/*model.cpp) \ $$files($$PWD/modules/value-editor/embedded*.cpp) \ $$files($$PWD/modules/value-editor/textcharformat.cpp) \ $$files($$PWD/modules/value-editor/syntaxhighlighter.cpp) \ $$files($$PWD/modules/bulk-operations/*.cpp) \ $$files($$PWD/modules/bulk-operations/operations/*.cpp) \ $$files($$PWD/modules/common/*.cpp) \ $$files($$PWD/modules/server-actions/*.cpp) \ HEADERS += \ $$PWD/app/app.h \ $$PWD/app/events.h \ $$PWD/app/apputils.h \ $$PWD/app/qmlutils.h \ $$PWD/app/jsonutils.h \ $$PWD/app/qcompress.h \ $$PWD/app/darkmode.h \ $$files($$PWD/app/models/*.h) \ $$files($$PWD/app/models/key-models/*.h) \ $$files($$PWD/modules/connections-tree/*.h) \ $$files($$PWD/modules/connections-tree/items/*.h) \ $$files($$PWD/modules/console/*.h) \ $$files($$PWD/modules/value-editor/*factory.h) \ $$files($$PWD/modules/value-editor/*model.h) \ $$files($$PWD/modules/value-editor/embedded*.h) \ $$files($$PWD/modules/value-editor/textcharformat.h) \ $$files($$PWD/modules/value-editor/syntaxhighlighter.h) \ $$files($$PWD/modules/*.h) \ $$files($$PWD/modules/bulk-operations/*.h) \ $$files($$PWD/modules/bulk-operations/operations/*.h) \ $$files($$PWD/modules/common/*.h) \ $$files($$PWD/modules/server-actions/*.h) \ $$PWD/modules/connections-tree/items/loadmoreitem.h THIRDPARTYDIR = $$PWD/../3rdparty/ include($$THIRDPARTYDIR/3rdparty.pri) exists( $$PWD/modules/crashpad/crashpad.pri ) { message("Build with Crashpad") include($$PWD/modules/crashpad/crashpad.pri) } release { message("Enable qtquickcompiler") CONFIG += qtquickcompiler } win32 { CONFIG += c++11 RC_ICONS = $$PWD/resources/images/logo.ico QMAKE_TARGET_COMPANY = resp.app QMAKE_TARGET_PRODUCT = RESP QMAKE_TARGET_DESCRIPTION = "RESP.app - Open source Developer GUI for Redis®" QMAKE_TARGET_COPYRIGHT = "Igor Malinovskiy (C) 2013-2022" release: DESTDIR = ./../bin/windows/release debug: DESTDIR = ./../bin/windows/debug LIBS += -ldwmapi } unix:macx { # OSX TARGET = "RESP" QT += svg CONFIG += c++11 debug: CONFIG-=app_bundle release: DESTDIR = ./../bin/osx/release debug: DESTDIR = ./../bin/osx/debug #deployment QMAKE_INFO_PLIST = $$PWD/resources/Info.plist ICON = $$PWD/resources/logo.icns } unix:!macx { # ubuntu & debian CONFIG += static release CONFIG -= debug DEFINES += DISABLE_SCALING_TEST QTPLUGIN += qsvg qsvgicon QMAKE_CXXFLAGS += -Wno-sign-compare release: DESTDIR = $$PWD/../bin/linux/release debug: DESTDIR = $$PWD/../bin/linux/debug #deployment LINUX_INSTALL_PATH = /opt/resp_app target.path = $$LINUX_INSTALL_PATH target.files = $$DESTDIR/resp INSTALLS += target exists( $$PWD/resources/qt.conf ) { appconfig.path = $$LINUX_INSTALL_PATH appconfig.files = $$PWD/resources/qt.conf INSTALLS += appconfig } data.path = $$LINUX_INSTALL_PATH/lib data.files = $$PWD/lib/* INSTALLS += data appicon.path = /usr/share/pixmaps/ appicon.files = $$PWD/resources/images/resp.png INSTALLS += appicon deskicon.path = /usr/share/applications deskicon.files = $$PWD/resources/resp.desktop INSTALLS += deskicon RESOURCES += $$PWD/resources/fonts.qrc } UI_DIR = $$DESTDIR/ui OBJECTS_DIR = $$DESTDIR/obj MOC_DIR = $$DESTDIR/obj RCC_DIR = $$DESTDIR/obj INCLUDEPATH += $$PWD/ \ $$PWD/modules/ \ $$UI_DIR/ \ RESOURCES += \ $$PWD/resources/images.qrc \ $$PWD/resources/icons.qrc \ $$PWD/qml/qml.qrc \ $$PWD/py/py.qrc \ $$PWD/resources/commands.qrc exists( $$PWD/resources/translations/rdm.qm ) { message("Translations found") RESOURCES += $$PWD/resources/tr.qrc } OTHER_FILES += \ qt.conf \ Info.plist \ qml\*.qml \ lupdate_only{ SOURCES += \ $$PWD/qml/*.qml \ $$PWD/qml/value-editor/*.qml \ $$PWD/qml/settings/*.qml \ $$PWD/qml/server-actions/*.qml \ $$PWD/qml/console/*.qml \ $$PWD/qml/connections/*.qml \ $$PWD/qml/connections-tree/*.qml \ $$PWD/qml/common/*.qml \ $$PWD/qml/bulk-operations/*.qml \ $$PWD/qml/extension-server/*.qml \ } TRANSLATIONS = \ $$PWD/resources/translations/rdm.ts \ $$PWD/resources/translations/rdm_zh_CN.ts \ $$PWD/resources/translations/rdm_zh_TW.ts \ $$PWD/resources/translations/rdm_es_ES.ts \ $$PWD/resources/translations/rdm_ja_JP.ts \ $$PWD/resources/translations/rdm_uk_UA.ts \ CODECFORSRC = UTF-8 ================================================ FILE: tests/py_tests/requirements.txt ================================================ ddt nose ================================================ FILE: tests/py_tests/test_formatters/test_msgpack_formatter.py ================================================ import io import json import unittest from ddt import ddt, data import msgpack from src.py.formatters.msgpack import MsgpackFormatter @ddt class TestMsgpackFormatter(unittest.TestCase): formatter = MsgpackFormatter() @data( [1, 2, 3, 4], [0, [25, 636905376000000000, 636906075333708700, 55.0, None]], [3925794820, 0, msgpack.Timestamp(seconds=1587539993, nanoseconds=518021200), False], [1, msgpack.ExtType(code=1, data=b'text')], [1, msgpack.ExtType(code=1, data=b'\x94\x01\x02\x03\x04')], # Example from #4781 b'\xdc\x00\x1f\xcd9\x07\x10\xcf\x08\xd8\x03\x9e\x8f\xf53t\xcd\x01\xae' b'\xce\x00\x08\x82:\xa47626\xcc\xca&\xcfH\xd8\x03\xa1\xc4C^\xba\xcfH' b'\xd8\x03\x9e\x90\x81\xbf\x01G\xc2\xcd\x05C\xce\x00\x02|Z\xc2\xc2\xcc' b'\xe5\xcb@kD\xc97r\x82+\x0b\xcb@S\xc0\x00\x00\x00\x00\x00\xa11' b'\xb4hhjjk c \xd0\xbf\xd0\xbe\xd1\x87\xd1\x82\xd0\xbe\xd0\xb9\x01\x85' b'\x04\xce\x00\x01\x98\xa9\x03\xce\x00\x02\xdfm\x02\xce\x00\x02\xbc]' b'\x01\xce\x00\x02\xa9Q\x00\xce\x00\x0b\x1b\xc5\xcd\x16n\xc0\xc0\xc2' b'\xc0\x92\x00\x91\x0b\xc0' ) def test_decode(self, val): if type(val) == bytes: msgpacked_val = val val = msgpack.loads(val, raw=False, strict_map_key=False) else: msgpacked_val = msgpack.dumps(val) expected_output = json.dumps(val, default=self.formatter.default, ensure_ascii=False) formatter_response_dict = self.formatter.decode(msgpacked_val) self.assertIn('output', formatter_response_dict) actual_output = formatter_response_dict['output'] self.assertEqual(actual_output, expected_output) @data( {'valid': ['valid', 'bytes'], 'extra': ['extra', 'bytes']}, {'valid': [], 'extra': ['extra', 'bytes']}, {'valid': msgpack.Timestamp(1, 1), 'extra': ['extra', 'bytes']}, ) def test_decode_stream(self, stream): expected_output = json.dumps(stream['valid'], default=self.formatter.default, ensure_ascii=False) buf = io.BytesIO() buf.write(msgpack.dumps(stream['valid'], use_bin_type=True)) buf.write(msgpack.dumps(stream['extra'], use_bin_type=True)) broken_val = buf.getvalue()[:-5] formatter_response_dict = self.formatter.decode(broken_val) self.assertIn('output', formatter_response_dict) actual_output = formatter_response_dict['output'] self.assertEqual(actual_output, expected_output) def test_encode(self): val = json.dumps('test') expected_output = msgpack.dumps('test') output = self.formatter.encode(val) self.assertEqual(output, expected_output) ================================================ FILE: tests/py_tests/test_formatters/test_php_formatter.py ================================================ import json import unittest from ddt import ddt, data import phpserialize from src.py.formatters.phpserialize import PhpSerializeFormatter @ddt class TestPhpSerializeFormatter(unittest.TestCase): formatter = PhpSerializeFormatter() @data( 'test', {'a': 1, 'b': 'ъъъ', 'c': None, 'd': '✓', 'e': {'f': {'g': '🔫', 'h': '喂'}}}, # Example from #4789 b'O:8:"stdClass":2:{s:3:"foo";s:3:"bar";s:3:"bar";s:3:"baz";}', # Example from #4942 b'cookieInfo|i:1;isWholesale|i:0;storageOnly|i:1;isLogged|i:0;' b'itemListType|s:3:"std";itemListNum|i:64;search|a:1:{s:5:"group";' b's:1:"2";}sr22|a:2:{s:3:"sex";s:7:"0,1,2,3";s:4:"size";s:68:' b'"17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,' b'37,38,39";}' # Foo(foo=Bar(bar=Foo(foo={'a': 1, 'b': 2}), open=False)) b'O:3:"Foo":1:{s:3:"foo";O:3:"Bar":2:{s:3:"bar";O:3:"Foo":1:{s:3:"foo";' b'a:2:{s:1:"a";i:1;s:1:"b";i:2;}}s:4:"open";b:0;}}', # Foo(foo=Bar(bar='baz', open=True)) b'O:3:"Foo":1:{s:3:"foo";O:3:"Bar":2:{s:3:"bar";s:3:"baz";s:4:"open";' b'b:1;}}' # Foo(foo=Bar(bar=[1, 2], open=True)) b'O:3:"Foo":1:{s:3:"foo";O:3:"Bar":2:{s:3:"bar";a:2:{i:0;i:1;i:1;i:2;}' b's:4:"open";b:1;}}' ) def test_decode(self, val): if type(val) == bytes: serialized_val = val val = phpserialize.loads(val, decode_strings=True, object_hook=phpserialize.phpobject) if not isinstance(val, dict): val = val._asdict() else: serialized_val = phpserialize.dumps(val) expected_output = json.dumps(val, ensure_ascii=False, default=self.formatter.default) formatter_response_dict = self.formatter.decode(serialized_val) self.assertIn('output', formatter_response_dict) actual_output = formatter_response_dict['output'] self.assertEqual(actual_output, expected_output) @data( 'test', {'喂': 'test'}, ) def test_encode(self, val): formatter_input = json.dumps(val, ensure_ascii=False) expected_output = phpserialize.dumps(val) output = self.formatter.encode(formatter_input) self.assertEqual(output, expected_output) ================================================ FILE: tests/py_tests/test_formatters/test_pickle_formatter.py ================================================ from datetime import datetime import json import pickle import unittest from ddt import ddt, data import numpy as np import pandas as pd from src.py.formatters.pickle import PickleFormatter @ddt class TestPickleFormatter(unittest.TestCase): formatter = PickleFormatter() @data( [1, 2, 3, 4], datetime.now(), np.array([[1, 2], [3, 4]]), np.array([1, 2, 3], dtype=complex), pd.Series(np.random.randn(5), index=['a', 'b', 'c', 'd', 'e']), pd.Series({'b': 1, 'a': 0, 'c': 2}), pd.Series({'b': 1, 'a': 0, 'c': 2}, index=['b', 'c', 'd', 'a']), pd.Series(np.random.randn(5)), pd.Series(pd.date_range('20130101', periods=6)), pd.DataFrame(np.random.randn(6, 4), index=pd.date_range('20130101', periods=6), columns=list('ABCD')), ) def test_decode(self, val): if isinstance(val, pd.Series): expected_output = f'{str(type(val))[1:-1]}\n{val.to_string()}' elif isinstance(val, pd.DataFrame): html = val.to_html(render_links=True, border=0) expected_output = self.formatter.format_html_output(val, html) elif isinstance(val, np.ndarray): html = pd.DataFrame(val).to_html(render_links=True, border=0) expected_output = self.formatter.format_html_output(val, html) else: expected_output = json.dumps(val, default=self.formatter.default, ensure_ascii=False) pickled_val = pickle.dumps(val) formatter_response_dict = self.formatter.decode(pickled_val) self.assertIn('output', formatter_response_dict) actual_output = formatter_response_dict['output'] self.assertEqual(actual_output, expected_output) ================================================ FILE: tests/qml_tests/qml_tests.pro ================================================ TEMPLATE = app TARGET = qml_tests CONFIG += warn_on qmltestcase CONFIG-=app_bundle SOURCES += $$PWD/qml_test_runner.cpp \ setup.cpp PROJECT_ROOT = $$PWD/../..// INCLUDEPATH += $$PROJECT_ROOT/src $$PROJECT_ROOT/src/modules $$PROJECT_ROOT/3rdparty/qredisclient/src SOURCES += \ $$files($$PROJECT_ROOT/src/app/apputils.cpp) \ $$files($$PROJECT_ROOT/src/app/qmlutils.cpp) \ $$files($$PROJECT_ROOT/src/app/jsonutils.cpp) \ $$files($$PROJECT_ROOT/src/app/qcompress.cpp) \ $$files($$PROJECT_ROOT/src/modules/value-editor/textcharformat.cpp) \ $$files($$PROJECT_ROOT/src/modules/value-editor/syntaxhighlighter.cpp) \ $$files($$PROJECT_ROOT/src/modules/value-editor/largetextmodel.cpp) \ QT += core gui quick network concurrent charts release: DESTDIR = $$PROJECT_ROOT/bin/tests debug: DESTDIR = $$PROJECT_ROOT/bin/tests OBJECTS_DIR = $$DESTDIR/qml_obj MOC_DIR = $$DESTDIR/qml_obj RCC_DIR = $$DESTDIR/qml_obj OTHER_FILES = $$PWD/tst_*.qml HEADERS += \ setup.h \ $$files($$PROJECT_ROOT/src/app/qmlutils.h) \ $$files($$PROJECT_ROOT/src/modules/value-editor/textcharformat.h) \ $$files($$PROJECT_ROOT/src/modules/value-editor/syntaxhighlighter.h) \ $$files($$PROJECT_ROOT/src/modules/value-editor/largetextmodel.h) \ DISTFILES += \ tst_MultilineEditor.qml include($$PROJECT_ROOT/3rdparty/3rdparty.pri) ================================================ FILE: tests/qml_tests/setup.cpp ================================================ #include "setup.h" #include #include "modules/value-editor/syntaxhighlighter.h" #include "modules/value-editor/textcharformat.h" void Setup::qmlEngineAvailable(QQmlEngine *engine) { QCoreApplication::instance()->setOrganizationDomain("redisdesktop.com"); QCoreApplication::instance()->setOrganizationName("redisdesktop"); QCoreApplication::instance()->setApplicationName("RESP.app - Developer GUI for Redis"); qmlRegisterType("rdm.models", 1, 0, "SyntaxHighlighter"); qmlRegisterType("rdm.models", 1, 0, "TextCharFormat"); m_qmlUtils = QSharedPointer(new QmlUtils()); engine->rootContext()->setContextProperty("qmlUtils", m_qmlUtils.data()); m_testUtils = QSharedPointer(new TestUtils()); engine->rootContext()->setContextProperty("testUtils", m_testUtils.data()); } void TestUtils::removeAppSetting(const QString &category) { QSettings s; s.remove(category); } ================================================ FILE: tests/qml_tests/setup.h ================================================ #pragma once #include #include #include #include #include #include "app/qmlutils.h" class TestUtils : public QObject { Q_OBJECT public: Q_INVOKABLE void removeAppSetting(const QString& category); }; class Setup : public QObject { Q_OBJECT public: Setup() {} public slots: void qmlEngineAvailable(QQmlEngine *engine); private: QSharedPointer m_qmlUtils; QSharedPointer m_testUtils; }; ================================================ FILE: tests/qml_tests/tst_MultilineEditor.qml ================================================ import QtQuick 2.3 import QtQml.Models 2.13 import QtTest 1.0 import Qt.labs.settings 1.0 import rdm.models 1.0 import "./../../src/qml/value-editor/editors/formatters/" import "./../../src/qml/value-editor/editors/" TestCase { name: "FormatterTests" property string validJson: '{"test": 123}' SystemPalette { id: sysPalette } Settings { id: defaultFormatterSettings category: "formatter_overrides" function cleanup() { testUtils.removeAppSetting("formatter_overrides") defaultFormatterSettings.sync() } } Item { id: appSettings property string valueEditorFont: "sans-serif" } ValueFormatters { id: valueFormattersModel } QtObject { id: valueEditor property var item: QtObject { function isEdited() { return false; } } } MultilineEditor { id: editor // ValueEditor fake properties property bool isMultiRow: true property string keyType: "list" property string keyName: "fake_list" } function init() { defaultFormatterSettings.cleanup() editor.defaultFormatter = "auto" } function test_loadFormattedValue() { editor.loadFormattedValue(validJson) verify(editor.__formatterCombobox.currentText === "JSON") verify(editor.__textView.format === "json") verify(!editor.__textView.readOnly) verify(editor.__textView.textFormat === TextEdit.PlainText) } function test_loadFormattedValue_withDefaultFormatter() { editor.defaultFormatter = "HEX TABLE" defaultFormatterSettings.setValue(editor.lastSelectedFormatterSetting, "Plain Text") defaultFormatterSettings.sync() editor.loadFormattedValue(validJson) verify(editor.__formatterCombobox.currentText === "HEX TABLE") verify(editor.__textView.format === "html") verify(editor.__textView.readOnly) verify(editor.__textView.textFormat === TextEdit.RichText) } function test_loadFormattedValue_withLastSelectedFormatter() { editor.defaultFormatter = "last_used" defaultFormatterSettings.setValue(editor.lastSelectedFormatterSetting, "Plain Text") defaultFormatterSettings.sync() editor.loadFormattedValue(validJson) verify(editor.__formatterCombobox.currentText === "Plain Text") verify(editor.__textView.format === "plain") verify(!editor.__textView.readOnly) verify(editor.__textView.textFormat === TextEdit.PlainText) } function test_loadFormattedValue_withLastSelectedFormatter_and_key_override() { defaultFormatterSettings.setValue(editor.keyName, "HEX") defaultFormatterSettings.setValue(editor.lastSelectedFormatterSetting, "Plain Text") defaultFormatterSettings.sync() editor.loadFormattedValue(validJson) verify(editor.__formatterCombobox.currentText === "HEX") verify(editor.__textView.format === "plain") verify(!editor.__textView.readOnly) verify(editor.__textView.textFormat === TextEdit.PlainText) } } ================================================ FILE: tests/qml_tests/tst_formatters.qml ================================================ import QtQuick 2.3 import QtQml.Models 2.13 import QtTest 1.0 import "./../../src/qml/value-editor/editors/formatters/" TestCase { name: "FormatterTests" ValueFormatters { id: valueFormatters } function test_plain() { // given var plain = valueFormatters.get(0) var testValue = "plain_text!" // checks verify(plain.name.length !== 0, "title") plain.getFormatted(testValue, function (error, formatted, readOnly, format){ compare(formatted, testValue) }) plain.getRaw(testValue, function (error, plain){ compare(plain, testValue) }) } } ================================================ FILE: tests/smoke_test.bat ================================================ @echo off taskkill /f /im rdm.exe cd build/windows/installer/resources/ START /b rdm.exe TIMEOUT 30 tasklist /FI "IMAGENAME eq rdm.exe" 2>NUL | find /I /N "rdm.exe">NUL if "%ERRORLEVEL%"=="0" ( taskkill /f /im rdm.exe exit 0 ) ELSE (exit 1) ================================================ FILE: tests/tests.pro ================================================ TEMPLATE = subdirs SUBDIRS = unit_tests \ qml_tests \ ================================================ FILE: tests/unit_tests/generate_coverage_report ================================================ #!/bin/bash ROOT_DIR=`readlink -f ./../../` BASE_DIR=`pwd` OBJ_DIR=$ROOT_DIR/bin/tests/obj #Build export LDFLAGS="-lgcov -fprofile-arcs" export CPPFLAGS="-fprofile-arcs -ftest-coverage" rm -fR $ROOT_DIR/bin coverage* qmake && make -sj 4 # Generate coverage files for all files lcov --base-directory $BASE_DIR --capture --initial --directory $OBJ_DIR --output-file coverage.info.base # Run tests $ROOT_DIR/bin/tests/tests # Generate coverage files for files used in tests lcov --capture --directory $OBJ_DIR --base-directory $BASE_DIR --output-file coverage.info.run # Merge result lcov -a coverage.info.base -a coverage.info.run -o coverage.info # Remove unit tests and external files to get clean report lcov -e coverage.info "$ROOT_DIR/*" -o coverage.info.filtered lcov -r coverage.info.filtered "$ROOT_DIR/tests/*" -o coverage.info.filtered lcov -r coverage.info.filtered "$ROOT_DIR/bin/*" -o coverage.info.filtered lcov -r coverage.info.filtered "$ROOT_DIR/3rdparty/*" -o coverage.info.filtered # Generate HTML report rm -fR coverage_report || true mkdir coverage_report genhtml coverage.info.filtered --output-directory coverage_report open coverage_report/index.html ================================================ FILE: tests/unit_tests/main.cpp ================================================ #include #include // tests #include #include #include "testcases/app/test_configmanager.h" #include "testcases/app/test_connectionsmanager.h" #include "testcases/app/test_keymodels.h" #include "testcases/app/test_treeoperations.h" #include "testcases/app/test_apputils.h" #include "testcases/connections-tree/test_databaseitem.h" #include "testcases/connections-tree/test_model.h" #include "testcases/connections-tree/test_serveritem.h" #include "testcases/console/test_consolemodel.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); initRedisClient(); int allTestsResult = 0 // connections-tree module #ifndef Q_OS_WIN + QTest::qExec(new TestServerItem, argc, argv) + QTest::qExec(new TestDatabaseItem, argc, argv) #endif + QTest::qExec(new TestModel, argc, argv) // console module + QTest::qExec(new TestConsoleOperations, argc, argv) // app + QTest::qExec(new TestConnectionsManager, argc, argv) + QTest::qExec(new TestConfigManager, argc, argv) + QTest::qExec(new TestKeyModels, argc, argv) + QTest::qExec(new TestTreeOperations, argc, argv) + QTest::qExec(new TestAppUtils, argc, argv) ; if (allTestsResult == 0) qDebug() << "[Tests PASS]"; else qDebug() << "[Tests FAIL]"; return (allTestsResult != 0) ? 1 : 0; } ================================================ FILE: tests/unit_tests/respbasetestcase.h ================================================ #pragma once #include #include "basetestcase.h" #include "models/connectionconf.h" template static void fakeDeleter(T*) {} class RESPBaseTestCase : public BaseTestCase { Q_OBJECT protected: ServerConfig getDummyConfig(QString name = "test") { ServerConfig dummyConf("127.0.0.1", "", RedisClient::ConnectionConfig ::DEFAULT_REDIS_PORT, name); dummyConf.setTimeouts(2000, 2000); return dummyConf; } }; ================================================ FILE: tests/unit_tests/testcases/app/app-tests.pri ================================================ APP_SRC_DIR = $$PWD/../../../../src/app/ INCLUDEPATH += $$APP_SRC_DIR HEADERS += \ $$files($$PWD/test_*.h) \ $$APP_SRC_DIR/events.h \ $$APP_SRC_DIR/apputils.h \ $$APP_SRC_DIR/jsonutils.h \ $$APP_SRC_DIR/models/connectionsmanager.h \ $$APP_SRC_DIR/models/configmanager.h \ $$APP_SRC_DIR/models/connectionconf.h \ $$APP_SRC_DIR/models/connectiongroup.h \ $$APP_SRC_DIR/models/treeoperations.h \ $$APP_SRC_DIR/models/key-models/keyfactory.h \ $$APP_SRC_DIR/models/key-models/abstractkey.h \ $$APP_SRC_DIR/models/key-models/stringkey.h \ $$APP_SRC_DIR/models/key-models/listkey.h \ $$APP_SRC_DIR/models/key-models/listlikekey.h \ $$APP_SRC_DIR/models/key-models/setkey.h \ $$APP_SRC_DIR/models/key-models/stream.h \ $$APP_SRC_DIR/models/key-models/sortedsetkey.h \ $$APP_SRC_DIR/models/key-models/hashkey.h \ $$APP_SRC_DIR/models/key-models/rejsonkey.h \ $$APP_SRC_DIR/models/key-models/unknownkey.h \ $$APP_SRC_DIR/models/key-models/newkeyrequest.h \ SOURCES += \ $$files($$PWD/test_*.cpp) \ $$APP_SRC_DIR/events.cpp \ $$APP_SRC_DIR/jsonutils.cpp \ $$APP_SRC_DIR/models/connectionsmanager.cpp \ $$APP_SRC_DIR/models/configmanager.cpp \ $$APP_SRC_DIR/models/connectiongroup.cpp \ $$APP_SRC_DIR/models/connectionconf.cpp \ $$APP_SRC_DIR/models/treeoperations.cpp \ $$APP_SRC_DIR/models/key-models/keyfactory.cpp \ $$APP_SRC_DIR/models/key-models/stringkey.cpp \ $$APP_SRC_DIR/models/key-models/listkey.cpp \ $$APP_SRC_DIR/models/key-models/listlikekey.cpp \ $$APP_SRC_DIR/models/key-models/setkey.cpp \ $$APP_SRC_DIR/models/key-models/stream.cpp \ $$APP_SRC_DIR/models/key-models/sortedsetkey.cpp \ $$APP_SRC_DIR/models/key-models/hashkey.cpp \ $$APP_SRC_DIR/models/key-models/rejsonkey.cpp \ $$APP_SRC_DIR/models/key-models/unknownkey.cpp \ $$APP_SRC_DIR/models/key-models/newkeyrequest.cpp \ OTHER_FILES += \ $$PWD/connections.json ================================================ FILE: tests/unit_tests/testcases/app/connections.json ================================================ [ {"host":"127.0.0.1", "name": "local", "port": 6379} ] ================================================ FILE: tests/unit_tests/testcases/app/test_apputils.cpp ================================================ #include "test_apputils.h" #include "app/apputils.h" void TestAppUtils::testHumanReadableSize() { long long size = 3000000000; QString result = humanReadableSize(size); QCOMPARE(result, "3.00 GB"); } ================================================ FILE: tests/unit_tests/testcases/app/test_apputils.h ================================================ #pragma once #include "respbasetestcase.h" class TestAppUtils : public RESPBaseTestCase { Q_OBJECT private slots: void testHumanReadableSize(); }; ================================================ FILE: tests/unit_tests/testcases/app/test_configmanager.cpp ================================================ #include "test_configmanager.h" #include "app/models/configmanager.h" #include #include void TestConfigManager::testGetApplicationConfigPath() { #ifdef Q_OS_WIN QSKIP("SKIP ON Windows"); #endif // Given QTemporaryDir tmpDir; tmpDir.setAutoRemove(true); QString basePath = tmpDir.path(); ConfigManager manager(basePath); qDebug() << "Base path:" << basePath; bool check_path = true; // When QString actual_result = manager.getApplicationConfigPath("config.json", check_path); // Then // Check that path is valid QCOMPARE(QString("%1/.rdm/config.json").arg(basePath), actual_result); QCOMPARE(check_path, QFile::exists(actual_result)); } ================================================ FILE: tests/unit_tests/testcases/app/test_configmanager.h ================================================ #pragma once #include "basetestcase.h" class TestConfigManager : public BaseTestCase { Q_OBJECT private slots: void testGetApplicationConfigPath(); }; ================================================ FILE: tests/unit_tests/testcases/app/test_connectionsmanager.cpp ================================================ #include "test_connectionsmanager.h" #include #include #include #include #include "app/events.h" #include "models/connectionsmanager.h" TestConnectionsManager::TestConnectionsManager() {} void TestConnectionsManager::loadConnectionsConfigFromFile() { // given // json fixture QString configTestFile = "./unit_tests/testcases/app/connections.json"; auto events = QSharedPointer(new Events()); // when loads connections ConnectionsManager testManager(configTestFile, events); testManager.loadConnections(); // then QCOMPARE(testManager.size(), 1); } void TestConnectionsManager::saveConnectionsConfigToFile_data() { QTest::addColumn("connectionName"); QTest::newRow("simple") << "test"; QTest::newRow("unicode") << "❤❤❤༆"; } void TestConnectionsManager::saveConnectionsConfigToFile() { // given QFETCH(QString, connectionName); QString configTestFile = QString("%1/test_rdm.json").arg(QDir::tempPath()); QFile::remove(configTestFile); auto connectionConfig = getDummyConfig(connectionName); auto events = QSharedPointer(new Events()); ConnectionsManager testManager(configTestFile, events); // when // add new connection and save testManager.addNewConnection(connectionConfig, true); // load everything from scratch ConnectionsManager testManagerNew(configTestFile, events); testManagerNew.loadConnections(); QModelIndex testIndex = testManagerNew.index(0, 0, QModelIndex()); auto metadata = testManagerNew.data(testIndex, ConnectionsTree::Model::itemMetaData).toHash(); // then QCOMPARE(QFile::exists(configTestFile), true); QCOMPARE(testManagerNew.rowCount(), 1); QCOMPARE(metadata["name"], QVariant(connectionName)); } ================================================ FILE: tests/unit_tests/testcases/app/test_connectionsmanager.h ================================================ #pragma once #include "respbasetestcase.h" #include "value-editor/tabsmodel.h" class TestConnectionsManager : public RESPBaseTestCase { Q_OBJECT public: TestConnectionsManager(); private slots: void loadConnectionsConfigFromFile(); void saveConnectionsConfigToFile(); void saveConnectionsConfigToFile_data(); }; ================================================ FILE: tests/unit_tests/testcases/app/test_keymodels.cpp ================================================ #include "test_keymodels.h" #include "app/models/key-models/hashkey.h" #include "app/models/key-models/listkey.h" #include "app/models/key-models/setkey.h" #include "app/models/key-models/sortedsetkey.h" #include "app/models/key-models/stringkey.h" void TestKeyModels::testKeyFactory() { // given QFETCH(QStringList, validReplies); auto dummyConnection = getRealConnectionWithDummyTransporter(validReplies); // when QSharedPointer actualResult = getKeyModel(dummyConnection); // then QFETCH(QString, typeValid); QFETCH(int, ttlValid); QCOMPARE(actualResult.isNull(), false); QCOMPARE(actualResult->type(), typeValid); QCOMPARE(actualResult->getTTL(), ttlValid); } void TestKeyModels::testKeyFactory_data() { QTest::addColumn("validReplies"); QTest::addColumn("typeValid"); QTest::addColumn("ttlValid"); QTest::newRow("Valid string model w/o TTL") << (QStringList() << "+string\r\n" << ":-1\r\n") << "string" << -1; QTest::newRow("Valid string model w TTL") << (QStringList() << "+string\r\n" << ":100\r\n") << "string" << 100; QTest::newRow("Valid list model w/o TTL") << (QStringList() << "+list\r\n" << ":-1\r\n" << ":1\r\n") << "list" << -1; QTest::newRow("Valid set model w/o TTL") << (QStringList() << "+set\r\n" << ":-1\r\n" << ":1\r\n") << "set" << -1; QTest::newRow("Valid sorted set model w/o TTL") << (QStringList() << "+zset\r\n" << ":-1\r\n" << ":1\r\n") << "zset" << -1; QTest::newRow("Valid hash model w/o TTL") << (QStringList() << "+hash\r\n" << ":-1\r\n" << ":1\r\n") << "hash" << -1; } void TestKeyModels::testKeyFactoryAddKey() { // given QFETCH(QStringList, testReplies); QFETCH(QString, keyType); QFETCH(QVariantMap, row); auto connection = getRealConnectionWithDummyTransporter(testReplies); KeyFactory factory; NewKeyRequest r(connection, -1, QSharedPointer()); // when r.setKeyName("testKey"); r.setKeyType(keyType); r.setValue(row); factory.submitNewKeyRequest(r); wait(100); // then verifyExecutedCommandsCount(connection, testReplies.size() + 2); // 2 = ping + info } void TestKeyModels::testKeyFactoryAddKey_data() { QTest::addColumn("testReplies"); QTest::addColumn("keyType"); QTest::addColumn("row"); QVariantMap singleRow{{"value", "test"}}; QTest::newRow("string") << (QStringList() << "+OK\r\n") << "string" << singleRow; QTest::newRow("list") << (QStringList() << "+OK\r\n") << "list" << singleRow; QTest::newRow("set") << (QStringList() << "+OK\r\n") << "set" << singleRow; QVariantMap hashRow{{"value", "test"}, {"key", "test-key"}}; QTest::newRow("hash") << (QStringList() << ":1\r\n") << "hash" << hashRow; QVariantMap zsetRow{{"value", "test"}, {"score", 5.0}}; QTest::newRow("zset") << (QStringList() << "+OK\r\n") << "zset" << zsetRow; } void TestKeyModels::testValueLoading() { // given QFETCH(QStringList, testReplies); auto dummyConnection = getRealConnectionWithDummyTransporter(testReplies); QFETCH(int, testRow); QFETCH(int, testRole); QFETCH(unsigned long, validRowCount); QFETCH(bool, validIsMultiRow); // when QSharedPointer keyModel = getKeyModel(dummyConnection); QVERIFY(keyModel.isNull() == false); QVERIFY(keyModel->isMultiRow() == validIsMultiRow); bool callbackCalled = false; keyModel->loadRowsCount([keyModel, &callbackCalled, validRowCount](QString) { QVERIFY(keyModel->rowsCount() == validRowCount); keyModel->loadRows(0, keyModel->rowsCount(), [&callbackCalled](const QString&, unsigned long) { callbackCalled = true; }); }); wait(500); QVERIFY(callbackCalled); QVERIFY(keyModel->isRowLoaded(testRow)); QVariant actualResult = keyModel->getData(testRow, testRole); keyModel->clearRowCache(); // then QFETCH(QString, validData); QFETCH(QStringList, validColumns); QCOMPARE(actualResult.toString(), validData); QCOMPARE(keyModel->getColumnNames(), validColumns); QVERIFY(keyModel->getRoles().size() != 0); QVERIFY(keyModel->isRowLoaded(0) == false); } void TestKeyModels::testValueLoading_data() { QTest::addColumn("testReplies"); QTest::addColumn("testRow"); QTest::addColumn("testRole"); QTest::addColumn("validRowCount"); QTest::addColumn("validIsMultiRow"); QTest::addColumn("validData"); QTest::addColumn("validColumns"); QTest::newRow("Valid string model") << (QStringList() << "+string\r\n" << ":-1\r\n" << "$17\r\n__nice_test_data!\r\n") << 0 << Qt::UserRole + 1 << (unsigned long)1 << false << "__nice_test_data!" << (QStringList() << "value"); QTest::newRow("Valid list model") << (QStringList() << "+list\r\n" << ":-1\r\n" << ":2\r\n" << "*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n") << 1 << Qt::UserRole + 2 << (unsigned long)2 << true << "bar" << (QStringList() << "rowNumber" << "value"); QTest::newRow("Valid set model") << (QStringList() << "+set\r\n" << ":-1\r\n" << ":2\r\n" << "*2\r\n$1\r\n0\r\n*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n" << ":1\r\n" << ":1\r\n" << ":1\r\n" << ":1\r\n") << 1 << Qt::UserRole + 2 << (unsigned long)2 << true << "bar" << (QStringList() << "rowNumber" << "value"); QTest::newRow("Valid zset model") << (QStringList() << "+zset\r\n" << ":-1\r\n" << ":2\r\n" << "*4\r\n$3\r\nfoo\r\n$1\r\n1\r\n$3\r\nbar\r\n$1\r\n1\r\n") << 1 << Qt::UserRole + 2 << (unsigned long)2 << true << "bar" << (QStringList() << "rowNumber" << "value" << "score"); QTest::newRow("Valid hash model") << (QStringList() << "+hash\r\n" << ":-1\r\n" << ":2\r\n" << "*2\r\n$1\r\n0\r\n*4\r\n$3\r\nfoo\r\n$1\r\n1\r\n$" "3\r\nfoo\r\n$3\r\nbar\r\n") << 1 << Qt::UserRole + 3 << (unsigned long)2 << true << "bar" << (QStringList() << "rowNumber" << "key" << "value"); } void TestKeyModels::testKeyModelModifyRows() { // given QFETCH(QStringList, testReplies); QFETCH(QVariantMap, row); QFETCH(int, role); bool rowsCountLoaded = false; auto dummyConnection = getRealConnectionWithDummyTransporter(testReplies); // when QSharedPointer keyModel = getKeyModel(dummyConnection); QVERIFY(keyModel.isNull() == false); keyModel->loadRowsCount([keyModel, &rowsCountLoaded](QString) { rowsCountLoaded = true; keyModel->loadRows(0, 10, [](const QString& err, unsigned long) { if (!err.isEmpty()) { qWarning() << err; return; } }); }); wait(500); row["value"] = "fakeUpdate"; keyModel->updateRow(0, row, [](const QString& err) { if (!err.isEmpty()) { qWarning() << err; } }); wait(500); QVariant actualResult = keyModel->getData(0, role); // then QVERIFY(rowsCountLoaded); QVERIFY(actualResult.type() == QVariant::ByteArray); QCOMPARE(actualResult.toString(), QString("fakeUpdate")); } void TestKeyModels::testKeyModelModifyRows_data() { QTest::addColumn("testReplies"); QTest::addColumn("row"); QTest::addColumn("role"); QVariantMap stringRow; stringRow["value"] = "test"; QTest::newRow("Valid string model") << (QStringList() << "+string\r\n" << ":-1\r\n" << "$17\r\n__nice_test_data!\r\n" << "+OK\r\n") << stringRow << Qt::UserRole + 1; QVariantMap listRow; listRow["rowNumber"] = 0; listRow["value"] = "test"; QTest::newRow("Valid list model") << (QStringList() << "+list\r\n" << ":-1\r\n" << ":2\r\n" << "*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n" << "*1\r\n$3\r\nfoo\r\n" << "+OK\r\n") << listRow << Qt::UserRole + 2; QVariantMap setRow; setRow["rowNumber"] = 0; setRow["value"] = "test"; QTest::newRow("Valid set model") << (QStringList() << "+set\r\n" << ":-1\r\n" << ":2\r\n" << "*2\r\n$1\r\n0\r\n*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n" << ":1\r\n" << ":1\r\n") << setRow << Qt::UserRole + 2; QVariantMap zsetRow; zsetRow["rowNumber"] = 0; zsetRow["value"] = "test"; zsetRow["score"] = 1.1; QTest::newRow("Valid zset model") << (QStringList() << "+zset\r\n" << ":-1\r\n" << ":2\r\n" << "*4\r\n$3\r\nfoo\r\n$1\r\n1\r\n$3\r\nbar\r\n$1\r\n1\r\n" << ":1\r\n" << ":1\r\n") << zsetRow << Qt::UserRole + 2; QVariantMap hashRow; hashRow["rowNumber"] = 0; hashRow["key"] = "test"; hashRow["value"] = "test"; QTest::newRow("Valid hash model") << (QStringList() << "+hash\r\n" << ":-1\r\n" << ":2\r\n" << "*2\r\n$1\r\n0\r\n*4\r\n$3\r\nfoo\r\n$1\r\n1\r\n$" "3\r\nbar\r\n$1\r\n1\r\n" << ":1\r\n" << ":1\r\n") << hashRow << Qt::UserRole + 3; } QSharedPointer TestKeyModels::getKeyModel( QSharedPointer connection) { QSharedPointer actualResult; KeyFactory factory; factory.loadKey(connection, "testKey", -1, [&actualResult](QSharedPointer model, const QString&) { actualResult = model; }); wait(100); return actualResult; } ================================================ FILE: tests/unit_tests/testcases/app/test_keymodels.h ================================================ #pragma once #include "basetestcase.h" #include "models/key-models/keyfactory.h" class TestKeyModels : public BaseTestCase { Q_OBJECT private slots: void testKeyFactory(); void testKeyFactory_data(); void testKeyFactoryAddKey(); void testKeyFactoryAddKey_data(); void testValueLoading(); void testValueLoading_data(); void testKeyModelModifyRows(); void testKeyModelModifyRows_data(); private: QSharedPointer getKeyModel(QSharedPointer connection); }; ================================================ FILE: tests/unit_tests/testcases/app/test_treeoperations.cpp ================================================ #include "test_treeoperations.h" #include #include #include "app/events.h" #include "connections-tree/items/databaseitem.h" #include "connections-tree/model.h" #include "models/connectionconf.h" #include "models/treeoperations.h" using namespace fakeit; using namespace ConnectionsTree; void TestTreeOperations::testCreation() { // given auto events = QSharedPointer(new Events()); auto config = getDummyConfig(); // when TreeOperations operations(config, events); // then // all ok Q_UNUSED(operations); } void TestTreeOperations::testGetDatabases() { // given auto events = QSharedPointer(new Events()); QString infoResp = getBulkStringReply( "# CPU\n" "used_cpu_sys:17.89\n" "used_cpu_user:24.70\n" "used_cpu_sys_children:0.06\n" "used_cpu_user_children:0.33\n\n" "# Keyspace\n" "db0:keys=3495,expires=0,avg_ttl=0\n" "db9:keys=1,expires=0,avg_ttl=0\n"); QStringList expectedResponses{infoResp, infoResp, "+OK\r\n", "+OK\r\n", "+OK\r\n", "-ERROR\r\n"}; auto connection = getFakeConnection(); connection->setFakeResponses(expectedResponses); connection->setClone(connection); // Fake callback RedisClient::DatabaseList result; Mock fake; TreeItem& owner = fake.get(); auto fakeOwner = QSharedPointer(&owner, fakeDeleter); auto callback = QSharedPointer( new Operations::GetDatabasesCallback( fakeOwner, [&result](Operations::DbMapping r, const QString&) { result = r; })); // when qDebug() << "testGetDatabases - start execution"; TreeOperations operations(getDummyConfig(), events); operations.setConnection(connection); operations.getDatabases(callback); // then wait(100); connection.clear(); QCOMPARE(result.size(), 13); } void TestTreeOperations::testLoadNamespaceItems() { // given QFETCH(uint, runCommandCalled); QFETCH(uint, retrieveCollectionCalled); QFETCH(QList, expectedScanResponses); QFETCH(QStringList, expectedResponses); // Setup dummy connection with on/off lua loading auto connection = getFakeConnection(expectedScanResponses, expectedResponses); ServerConfig conf; connection->setConnectionConfig(conf); auto events = QSharedPointer(new Events()); QSharedPointer operations( new TreeOperations(getDummyConfig(), events)); operations->setConnection(connection); // Fake callback RedisClient::Connection::RawKeysList result; Mock fake; TreeItem& owner = fake.get(); auto fakeOwner = QSharedPointer(&owner, fakeDeleter); auto callback = QSharedPointer( new Operations::LoadNamespaceItemsCallback( fakeOwner, [&result](const RedisClient::Connection::RawKeysList& r, const QString&) { result = r; })); // when operations->loadNamespaceItems( 0, QString("*"), callback); // then - part 1 wait(5); QCOMPARE(result.size(), 2); QCOMPARE(connection->runCommandCalled, runCommandCalled); QCOMPARE(connection->retrieveCollectionCalled, retrieveCollectionCalled); } void TestTreeOperations::testLoadNamespaceItems_data() { QTest::addColumn("runCommandCalled"); QTest::addColumn("retrieveCollectionCalled"); QTest::addColumn >("expectedScanResponses"); QTest::addColumn("expectedResponses"); QTest::newRow("SCAN execution") << 1u << 1u << (QList() << QVariant(QVariantList() << QString("test") << QString("test2"))) << (QStringList() << "+OK\r\n"); } void TestTreeOperations::testFlushDb() { // given auto events = QSharedPointer(new Events()); auto connection = getFakeConnection(QList() << QVariant(), QStringList() << "+OK\r\n"); // Mock callback bool callbackCalledWithError = false; Mock fake; TreeItem& owner = fake.get(); auto fakeOwner = QSharedPointer(&owner, fakeDeleter); auto callback = QSharedPointer( new Operations::FlushDbCallback( fakeOwner, [&callbackCalledWithError](const QString& e) { callbackCalledWithError = !e.isEmpty(); })); // when TreeOperations operations(getDummyConfig(), events); operations.setConnection(connection); operations.flushDb(0, callback); // then - part 1 wait(5); QCOMPARE(callbackCalledWithError, false); QCOMPARE(connection->runCommandCalled, 1u); QCOMPARE(connection->executedCommands[0].getPartAsString(0), QString("FLUSHDB")); } void TestTreeOperations::testFlushDbCommandError() { // given auto events = QSharedPointer(new Events()); auto connection = getFakeConnection(); connection->returnErrorOnCmdRun = true; // Fake callback bool callbackCalledWithError = false; Mock fake; TreeItem& owner = fake.get(); auto fakeOwner = QSharedPointer(&owner, fakeDeleter); auto callback = QSharedPointer( new Operations::FlushDbCallback( fakeOwner, [&callbackCalledWithError](const QString& e) { callbackCalledWithError = !e.isEmpty(); })); // when TreeOperations operations(getDummyConfig(), events); operations.setConnection(connection); operations.flushDb(0, callback); // then - part 1 wait(5); QCOMPARE(callbackCalledWithError, true); } ================================================ FILE: tests/unit_tests/testcases/app/test_treeoperations.h ================================================ #pragma once #include "respbasetestcase.h" class TestTreeOperations : public RESPBaseTestCase { Q_OBJECT private slots: void testCreation(); void testGetDatabases(); void testLoadNamespaceItems(); void testLoadNamespaceItems_data(); void testFlushDb(); void testFlushDbCommandError(); }; ================================================ FILE: tests/unit_tests/testcases/connections-tree/connections-tree-tests.pri ================================================ CONNECTIONS_TREE_SRC_DIR = $$PWD/../../../../src/modules/connections-tree/ HEADERS += \ $$PWD/mocks.h \ $$files($$PWD/test_*.h) \ $$files($$CONNECTIONS_TREE_SRC_DIR/items/*.h) \ $$CONNECTIONS_TREE_SRC_DIR/operations.h \ $$CONNECTIONS_TREE_SRC_DIR/utils.h \ $$CONNECTIONS_TREE_SRC_DIR/keysrendering.h \ $$CONNECTIONS_TREE_SRC_DIR/model.h \ SOURCES += \ $$PWD/mocks.cpp \ $$files($$PWD/test_*.cpp) \ $$files($$CONNECTIONS_TREE_SRC_DIR/items/*.cpp) \ $$CONNECTIONS_TREE_SRC_DIR/utils.cpp \ $$CONNECTIONS_TREE_SRC_DIR/keysrendering.cpp \ $$CONNECTIONS_TREE_SRC_DIR/model.cpp \ ================================================ FILE: tests/unit_tests/testcases/connections-tree/mocks.cpp ================================================ #include "mocks.h" #include #include Mock getOperations() { Mock operations; When(Method(operations, getNamespaceSeparator)).AlwaysReturn(":"); When(Method(operations, defaultFilter)).AlwaysReturn("*"); When(Method(operations, mode)).AlwaysReturn("default"); When(Method(operations, disconnect)).Return(); When(Method(operations, notifyDbWasUnloaded)).Return(); return operations; } Mock getOperationsWithGetDatabases( RedisClient::DatabaseList db, const QString &err) { auto op = getOperations(); When(Method(op, getDatabases)) .AlwaysDo( [db, err]( QSharedPointer cb) -> QFuture { cb->rawCallback()(db, err); return QFuture(); }); return op; } Mock getOperationsWithDbAndKeys( RedisClient::DatabaseList db, const QString &err, QList keys) { auto op = getOperationsWithGetDatabases(db, err); When(Method(op, loadNamespaceItems)) .Do([keys, err]( uint, const QString &, QSharedPointer< ConnectionsTree::Operations::LoadNamespaceItemsCallback> cb) -> void { cb->rawCallback()(keys, err); }); return op; } ================================================ FILE: tests/unit_tests/testcases/connections-tree/mocks.h ================================================ #pragma once #include #include "connections-tree/operations.h" using namespace fakeit; Mock getOperations(); Mock getOperationsWithGetDatabases( RedisClient::DatabaseList db, const QString& err); Mock getOperationsWithDbAndKeys( RedisClient::DatabaseList db, const QString& err, QList keys); ================================================ FILE: tests/unit_tests/testcases/connections-tree/test_databaseitem.cpp ================================================ #include "test_databaseitem.h" #include #include #include #include #include #include "connections-tree/items/databaseitem.h" #include "connections-tree/items/serveritem.h" #include "connections-tree/model.h" #include "mocks.h" using namespace ConnectionsTree; TestDatabaseItem::TestDatabaseItem(QObject* parent) : QObject(parent) {} void TestDatabaseItem::testLoadKeys() { // given QList keys; for (int i = 1; i < 100000; i++) { keys.append(QString("test-%1-key").arg(i).toUtf8()); } keys.append("test-2-key"); keys.append("test-2-key:subkey"); keys.append("test-2-key:namespace:subkey2"); auto operations = getOperationsWithDbAndKeys({{0, 55}}, QString(), keys); Operations& mock = operations.get(); auto ptr = QSharedPointer(&mock, fakeDeleter); Model dummyModel; QSharedPointer parentItem(new ServerItem(ptr, dummyModel)); parentItem->setWeakPointer(parentItem.toWeakRef()); // when parentItem->handleEvent("click"); QTest::qWait(150); qDebug() << parentItem->childCount(); QSharedPointer item = parentItem->child(0); item->handleEvent("click"); QTest::qWait(150); // then // TODO: check mock calls // TODO: verify "load more" item is last // TODO: verify namespaces are rendered correctly QCOMPARE(item->childCount(), (unsigned int)1002); QCOMPARE(item->getDisplayName(), QString("db0 (55)")); QCOMPARE(item->getAllChilds().isEmpty(), false); QCOMPARE(item->isEnabled(), true); QCOMPARE(item->isLocked(), false); } ================================================ FILE: tests/unit_tests/testcases/connections-tree/test_databaseitem.h ================================================ #pragma once #include class TestDatabaseItem : public QObject { Q_OBJECT public: explicit TestDatabaseItem(QObject *parent = 0); private slots: void testLoadKeys(); }; ================================================ FILE: tests/unit_tests/testcases/connections-tree/test_model.cpp ================================================ #include "test_model.h" #include "connections-tree/model.h" #include void TestModel::testLoadImplementation() { // Given ConnectionsTree::Model m; // When auto test = QScopedPointer(new QAbstractItemModelTester( (QAbstractItemModel *)&m, QAbstractItemModelTester::FailureReportingMode::Fatal, this)); // Then // No assertions Q_UNUSED(test); } ================================================ FILE: tests/unit_tests/testcases/connections-tree/test_model.h ================================================ #pragma once #include class TestModel : public QObject { Q_OBJECT private slots: void testLoadImplementation(); }; ================================================ FILE: tests/unit_tests/testcases/connections-tree/test_serveritem.cpp ================================================ #include "test_serveritem.h" #include #include #include #include #include "respbasetestcase.h" #include "connections-tree/items/serveritem.h" #include "connections-tree/model.h" #include "mocks.h" using namespace ConnectionsTree; using namespace fakeit; TestServerItem::TestServerItem(QObject* parent) : QObject(parent) {} void TestServerItem::testLoad() { // given QMap databases = {{0, 55}}; Mock operations = getOperationsWithGetDatabases(databases, QString()); Operations& mock = operations.get(); auto ptr = QSharedPointer(&mock, fakeDeleter); QFETCH(QString, action); Model dummyModel; ServerItem item{ptr, dummyModel}; // when item.handleEvent(action); QTest::qWait(50); // then QCOMPARE(item.childCount(), static_cast(1)); QCOMPARE(item.child(0)->getDisplayName(), QString("db0 (55)")); QCOMPARE(item.isLocked(), false); QCOMPARE(item.isDatabaseListLoaded(), true); } void TestServerItem::testLoad_data() { QTest::addColumn("action"); QTest::newRow("load") << "click"; QTest::newRow("reload") << "reload"; } void TestServerItem::testBasicMethods() { // given Mock operations; When(Method(operations, connectionName)).Return("test"); Operations& mock = operations.get(); auto ptr = QSharedPointer(&mock, fakeDeleter); Model dummyModel; // when ServerItem item(ptr, dummyModel); // then QCOMPARE(item.getDisplayName(), QString("test")); QCOMPARE(item.parent() == nullptr, true); QCOMPARE(item.isEnabled(), true); QCOMPARE(item.isLocked(), false); QCOMPARE(item.child(0).isNull(), true); QCOMPARE(item.getAllChilds().isEmpty(), true); QCOMPARE(item.row(), 0); } void TestServerItem::testLoad_invalid() { // given Mock operations = getOperationsWithGetDatabases({}, QString("fake connection error")); Operations& mock = operations.get(); auto ptr = QSharedPointer(&mock, fakeDeleter); Model dummyModel; ServerItem item{ptr, dummyModel}; // when item.handleEvent("click"); QTest::qWait(50); // then // TODO: check mock calls QCOMPARE(item.childCount(), static_cast(0)); QCOMPARE(item.isLocked(), false); QCOMPARE(item.isDatabaseListLoaded(), false); } void TestServerItem::testUnload() { // given QMap databases = {{0, 55}}; Mock operations = getOperationsWithGetDatabases(databases, QString()); Operations& mock = operations.get(); auto ptr = QSharedPointer(&mock, fakeDeleter); Model dummyModel; ServerItem item{ptr, dummyModel}; // when item.handleEvent("click"); QTest::qWait(50); item.handleEvent("unload"); // then // TODO: check mock calls QCOMPARE(item.childCount(), static_cast(0)); QCOMPARE(item.isLocked(), false); QCOMPARE(item.isDatabaseListLoaded(), false); } ================================================ FILE: tests/unit_tests/testcases/connections-tree/test_serveritem.h ================================================ #pragma once #include class TestServerItem : public QObject { Q_OBJECT public: explicit TestServerItem(QObject *parent = 0); private slots: void testLoad(); void testLoad_data(); void testLoad_invalid(); void testUnload(); void testBasicMethods(); }; ================================================ FILE: tests/unit_tests/testcases/console/console-tests.pri ================================================ CONSOLE_SRC_DIR = $$PWD/../../../../src/modules/console/ HEADERS += \ $$files($$PWD/*.h) \ $$CONSOLE_SRC_DIR/consolemodel.h \ SOURCES += \ $$files($$PWD/*.cpp) \ $$CONSOLE_SRC_DIR/consolemodel.cpp \ ================================================ FILE: tests/unit_tests/testcases/console/test_consolemodel.cpp ================================================ #include #include #include "console/consolemodel.h" #include "test_consolemodel.h" void TestConsoleOperations::init_invalid() { //given QSharedPointer invalidConnection = getFakeConnection( QList(), QStringList(), 2.6, true ).dynamicCast(); Console::Model testModel(invalidConnection, 0, QList()); QSignalSpy spy(&testModel, SIGNAL(addOutput(const QString&, QString))); //when testModel.init(); //then QCOMPARE(spy.count(), 1); QList arguments = spy.takeFirst(); QVERIFY(arguments.at(0).toString() == "Invalid Connection. Check connection settings."); } ================================================ FILE: tests/unit_tests/testcases/console/test_consolemodel.h ================================================ #pragma once #include "basetestcase.h" class TestConsoleOperations : public BaseTestCase { Q_OBJECT private slots: void init_invalid(); }; ================================================ FILE: tests/unit_tests/testcases/value-editor/value-editor-tests.pri ================================================ VALUEEDITOR_SRC_DIR = $$PWD/../../../../src/modules/value-editor/ HEADERS += \ $$files($$VALUEEDITOR_SRC_DIR/*.h) \ SOURCES += \ $$files($$VALUEEDITOR_SRC_DIR/*.cpp) \ ================================================ FILE: tests/unit_tests/unit_tests.pro ================================================ QT += core gui network concurrent widgets quick quickwidgets testlib TARGET = tests TEMPLATE = app CONFIG += debug c++17 CONFIG-=app_bundle PROJECT_ROOT = $$PWD/../..// SRC_DIR = $$PROJECT_ROOT/src// HEADERS += \ $$PROJECT_ROOT/3rdparty/qredisclient/tests/unit_tests/basetestcase.h \ $$files($$PROJECT_ROOT/3rdparty/qredisclient/tests/unit_tests/mocks/*.h) \ $$files($$PROJECT_ROOT/src/modules/common/*.h) \ $$files($$PWD/*.h) \ SOURCES += \ $$PROJECT_ROOT/3rdparty/qredisclient/tests/unit_tests/basetestcase.cpp \ $$files($$PROJECT_ROOT/3rdparty/qredisclient/tests/unit_tests/mocks/*.cpp) \ $$files($$PROJECT_ROOT/src/modules/common/*.cpp) \ $$PWD/main.cpp \ INCLUDEPATH += $$SRC_DIR/modules/ \ $$SRC_DIR/ \ $$PWD/ \ $$PROJECT_ROOT/3rdparty/qredisclient/tests/unit_tests/ \ $$PROJECT_ROOT/3rdparty/fakeit/single_header/qtest/ DEFINES += INTEGRATION_TESTS #TEST CASES include($$PWD/testcases/app/app-tests.pri) include($$PWD/testcases/connections-tree/connections-tree-tests.pri) include($$PWD/testcases/console/console-tests.pri) include($$PWD/testcases/value-editor/value-editor-tests.pri) ############# include($$PROJECT_ROOT/3rdparty/3rdparty.pri) release: DESTDIR = $$PROJECT_ROOT/bin/tests debug: DESTDIR = $$PROJECT_ROOT/bin/tests UI_DIR = $$DESTDIR/ui OBJECTS_DIR = $$DESTDIR/obj MOC_DIR = $$DESTDIR/obj RCC_DIR = $$DESTDIR/obj