├── .DS_Store ├── .github └── workflows │ └── objective-c-xcode.yml ├── .gitignore ├── .version ├── CHANGELOG.md ├── LICENSE ├── Q&A.md ├── README.md ├── README.zh_CN.md ├── Tests ├── RedisBaseTests │ ├── RedisBaseTest.swift │ └── RedisClientBaseTest.swift ├── RedisCommandTests │ └── RedisClentStringTest.swift ├── Store │ ├── AppContextStoreTests.swift │ ├── KeysDelStoreTests.swift │ ├── RedisKeysStoreTests.swift │ └── StoreBaseTests.swift └── Tests.swift ├── redis-pro.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ ├── chengpan.xcuserdatad │ │ ├── IDEFindNavigatorScopes.plist │ │ ├── UserInterfaceState.xcuserstate │ │ ├── WorkspaceSettings.xcsettings │ │ ├── xcdebugger │ │ │ └── Expressions.xcexplist │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ ├── chengpanwang.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings │ │ └── chenpanwang.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── xcshareddata │ └── xcschemes │ │ └── redis-pro.xcscheme └── xcuserdata │ ├── chengpan.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ ├── chengpanwang.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ └── chenpanwang.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist └── redis-pro ├── .DS_Store ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ ├── Contents.json │ ├── redis@128.png │ ├── redis@256.png │ ├── redis@32-1.png │ ├── redis@32.png │ └── redis@64.png ├── Contents.json └── icon-redis.imageset │ ├── Contents.json │ ├── redis@1x.png │ ├── redis@2x.png │ └── redis@3x.png ├── Command ├── AbountCommand.swift ├── CheckUpdateCommand.swift ├── HomeCommand.swift └── RedisProCommands.swift ├── Common ├── Assert.swift ├── BizError.swift ├── Const.swift ├── DoubleFormatter.swift ├── Enums │ ├── ButtonTypeEnum.swift │ ├── MainViewTypeEnum.swift │ ├── RedisConnectionTypeEnum.swift │ ├── RedisKeyTypeEnum.swift │ └── TableContextMenuEnum.swift ├── Helpers │ ├── DateHelper.swift │ ├── NumberHelper.swift │ ├── PasteboardHelper.swift │ └── StringHelper.swift ├── LoggerFactory.swift ├── MTheme.swift ├── NIO │ ├── ReadTimeoutHandler.swift │ ├── ReconnectHandler.swift │ └── TimeoutHandler.swift ├── NetworkMonitor.swift ├── Objects.swift ├── PuppyLogger.swift ├── RedisClient │ ├── RediStackClient.swift │ ├── RediStackClientStream.swift │ ├── RedisClientConfig.swift │ ├── RedisClientConn.swift │ ├── RedisClientHash.swift │ ├── RedisClientKeys.swift │ ├── RedisClientList.swift │ ├── RedisClientLua.swift │ ├── RedisClientSSH.swift │ ├── RedisClientScan.swift │ ├── RedisClientSet.swift │ ├── RedisClientSlowLog.swift │ ├── RedisClientString.swift │ ├── RedisClientSystem.swift │ ├── RedisClientZSet.swift │ └── RedisCommandExt.swift ├── SSH │ ├── SSHAuthentication.swift │ ├── SSHForward.swift │ ├── SSHRediStackClient.swift │ └── SSHTunnel.swift ├── Settings │ ├── ColorSchemeEnum.swift │ └── RedisFavoriteDefaultSelectTypeEnum.swift ├── Stopwatch.swift ├── StringFormatter.swift ├── UserDefaults │ ├── RedisDefaults.swift │ └── UserDefaultsKeysEnum.swift └── VersionManager.swift ├── Info.plist ├── Language ├── MButton.strings │ ├── en.xcloc │ │ ├── Localized Contents │ │ │ └── en.xliff │ │ ├── Source Contents │ │ │ └── redis-pro │ │ │ │ ├── Views │ │ │ │ └── Components │ │ │ │ │ └── en.lproj │ │ │ │ │ └── MButton.strings │ │ │ │ └── en.lproj │ │ │ │ ├── InfoPlist.strings │ │ │ │ └── Localizable.strings │ │ └── contents.json │ └── zh-Hans.xcloc │ │ ├── Localized Contents │ │ └── zh-Hans.xliff │ │ ├── Source Contents │ │ └── redis-pro │ │ │ ├── Views │ │ │ └── Components │ │ │ │ └── en.lproj │ │ │ │ └── MButton.strings │ │ │ └── en.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ │ └── contents.json └── redis-pro │ ├── en.xcloc │ ├── Localized Contents │ │ └── en.xliff │ ├── Source Contents │ │ └── redis-pro │ │ │ ├── Language │ │ │ └── en.lproj │ │ │ │ ├── Localizble.strings │ │ │ │ └── RedisInfo.strings │ │ │ └── en.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ └── contents.json │ └── zh-Hans.xcloc │ ├── Localized Contents │ └── zh-Hans.xliff │ ├── Source Contents │ └── redis-pro │ │ ├── Language │ │ └── en.lproj │ │ │ ├── Localizble.strings │ │ │ └── RedisInfo.strings │ │ └── en.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ └── contents.json ├── Model ├── ClientModel.swift ├── GlobalContext.swift ├── Page.swift ├── RedisConfigItemModel.swift ├── RedisFavoriteModel.swift ├── RedisHashEntryModel.swift ├── RedisInfoItemModel.swift ├── RedisInfoModel.swift ├── RedisInsanceModel.swift ├── RedisKeyModel.swift ├── RedisKeyValueModel.swift ├── RedisListItemModel.swift ├── RedisModel.swift ├── RedisZSetItemModel.swift ├── ScanModel.swift └── SlowLogModel.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── Store ├── AppContextStore.swift ├── AppStore.swift ├── ClientListStore.swift ├── DatabaseStore.swift ├── DependencyKeys.swift ├── FavoriteStore.swift ├── HashValueStore.swift ├── KeyObjectStore.swift ├── KeyStore.swift ├── ListValueStore.swift ├── LoadingStore.swift ├── LoginStore.swift ├── LuaStore.swift ├── PageStore.swift ├── RedisConfigStore.swift ├── RedisInfoStore.swift ├── RedisKeysStore.swift ├── RedisSystemStore.swift ├── RenameStore.swift ├── SetValueStore.swift ├── SettingsStore.swift ├── SlowLogStore.swift ├── StringValueStore.swift ├── SystemEnvironment.swift ├── TableStore.swift ├── ValueStore.swift └── ZSetValueStore.swift ├── Views ├── .DS_Store ├── App │ ├── AboutView.swift │ └── Settings │ │ └── SettingsView.swift ├── Components │ ├── Extensions.swift │ ├── Form │ │ ├── FormItemDouble.swift │ │ ├── FormItemInt.swift │ │ ├── FormItemPassword.swift │ │ ├── FormItemText.swift │ │ ├── FormItemTextArea.swift │ │ ├── FormLabel.swift │ │ ├── FormText.swift │ │ ├── FormWrapper.swift │ │ ├── MDoubleField.swift │ │ ├── MIntField.swift │ │ ├── MPasswordField.swift │ │ ├── MSecureField.swift │ │ ├── MTextEditor.swift │ │ ├── MTextField.swift │ │ ├── NIntField.swift │ │ ├── NPasswordField.swift │ │ ├── NSearchField.swift │ │ ├── NSecureField.swift │ │ ├── NTextEditor.swift │ │ └── NTextField.swift │ ├── IconButton.swift │ ├── KeyObjectBar.swift │ ├── LoadingView.swift │ ├── Loadings.swift │ ├── MButton.swift │ ├── MIcon.swift │ ├── MLabel.swift │ ├── MLoading.swift │ ├── MSpin.swift │ ├── MTabView.swift │ ├── Messages.swift │ ├── ModalView.swift │ ├── PageBar.swift │ ├── RedisKeyTypePicker.swift │ ├── SearchBar.swift │ ├── Table │ │ ├── NTable.swift │ │ ├── NTableColumn.swift │ │ ├── TableCellView.swift │ │ └── TableColumnType.swift │ ├── Tag.swift │ ├── TextViewController.swift │ └── TextViewController.xib ├── HomeView.swift ├── IndexView.swift ├── Login │ ├── LoginForm.swift │ ├── LoginView.swift │ └── RedisListView.swift ├── RedisEditorView │ ├── HashEditorView.swift │ ├── ListEditorView.swift │ ├── RedisValueEditView.swift │ ├── RedisValueHeaderView.swift │ ├── RedisValueView.swift │ ├── SetEditorView.swift │ ├── StringEditorView.swift │ └── ZSetEditorView.swift ├── Sidebar │ ├── DatabasePicker.swift │ └── RedisKeysListView.swift └── System │ ├── ClientsListView.swift │ ├── LuaView.swift │ ├── RedisConfigView.swift │ ├── RedisInfoView.swift │ ├── RedisSystemView.swift │ └── SlowLogView.swift ├── en.lproj └── Localizable.strings ├── redis-proRelease.entitlements ├── redis_pro.entitlements ├── redis_proApp.swift └── zh-Hans.lproj └── Localizable.strings /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/objective-c-xcode.yml: -------------------------------------------------------------------------------- 1 | name: Xcode - Build and Analyze 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | name: Build and analyse default scheme using xcodebuild command 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Set Default Scheme 18 | run: | 19 | scheme_list=$(xcodebuild -list -json | tr -d "\n") 20 | default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") 21 | echo $default | cat >default 22 | echo Using default scheme: $default 23 | - name: Build 24 | env: 25 | scheme: ${{ 'default' }} 26 | run: | 27 | if [ $scheme = default ]; then scheme=$(cat default); fi 28 | if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi 29 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` 30 | xcodebuild clean build analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" | xcpretty && exit ${PIPESTATUS[0]} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | redisProTests/RedisClientTest.swift 2 | .idea 3 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | { 2 | "latestVersionNum": 31, 3 | "latestVersion": "3.1.0", 4 | "updateType": "hint", 5 | "releaseNotes": "1. 增加批量删除功能。\n2. 编辑器样式调整。\n3. 修复查询空时, table select index 数值异常问题。 \n4. 刷新按钮时使用当前查询关键字。 \n5. 修复查询空时, table select index 数值异常问题。\n6. 优化查询时loading中断问题。\n7. 使用async优化确认框。" 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.4.1 2 | 1. 优化scan, 没有通配符时不进行scan搜索。感谢@ydq 3 | 2. 使用swift concurrency 重写部分功能。 4 | 5 | # 1.4.2 6 | 1. 密码,搜索框使用swiftUI 原生组件。 7 | 2. 表格增加双击编辑。 8 | 3. 修复hash field 禁用状态错误bug。 9 | 4. 去除promisekit依赖,使用swift concurrency。 10 | 11 | -------------------------------------------------------------------------------- /Q&A.md: -------------------------------------------------------------------------------- 1 | # 问题记录 2 | 3 | ## TCA 4 | 1. tca 监听不存在的属性值时, 会造成编译卡死, swift-frontend 内存泄漏 5 | 2. 同一个reducer被注入多次时, action方法会被多次调用,例如: 6 | ``` 7 | Scope(state: \.redisKeysState, action: /Action.redisKeysAction) { 8 | RedisKeysStore(redisInstanceModel: redisInstanceModel) 9 | } 10 | Scope(state: \.redisKeysState, action: /Action.redisKeysAction) { 11 | RedisKeysStore(redisInstanceModel: redisInstanceModel) 12 | } 13 | ``` 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Pro 2 | 3 | English | [简体中文](./README.zh_CN.md) 4 | 5 | ![Swift5.0+](https://img.shields.io/badge/Swift-5.0%2B-orange.svg?style=flat) 6 | [![release](https://img.shields.io/github/v/release/cmushroom/redis-pro?include_prereleases)](https://github.com/cmushroom/redis-pro/releases) 7 | ![platforms](https://img.shields.io/badge/Platforms-macOS%20-orange.svg?style=flat) 8 | [![Gitter](https://badges.gitter.im/redis-pro/community.svg)](https://gitter.im/redis-pro/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 9 | 10 | ## Intro 11 | * redis-pro is a simple and easy to use management of redis, written in SwiftUI. 12 | * [Sequel-Ace](https://github.com/Sequel-Ace/Sequel-Ace) and Aliyun DMS was borrowed in the development process,Sequel-Ace (Sequel-Pro) is a simple and easy to use mysql management 13 | 14 | ## Install 15 | * download package in release page 16 | [address](https://github.com/cmushroom/redis-pro/releases) 17 | 18 | * homebrew 19 | ``` 20 | brew install redis-pro 21 | ``` 22 | 23 | 24 | 25 | ## Platform 26 | support macos (Intel, Apple Silicon) platform, will support ipad os in the future 27 | 28 | ## Todo 29 | - [x] client list and kill 30 | - [x] homebrew install 31 | - [x] slow log 32 | - [x] redis config update 33 | - [x] ssh login 34 | - [x] favorite delete confirm 35 | - [x] TCA 36 | - [x] delete batch 37 | - [ ] terminal 38 | - [ ] ipad os support 39 | - [ ] ssh key 40 | 41 | 42 | ## Version 43 | * macos: >= 11.0 44 | * redis: 3.x¹ ... 6.x 45 | 46 | ## Dependency 47 | * RediStack a redis client wiritten in swiftNIO 48 | * swift-log swift log framework, need puppy implementation 49 | * Puppy log framework, support console, file 50 | * SwiftyJSON json convert 51 | 52 | 53 | 54 | ## Snapshot 55 | login 56 | 0 57 | 58 | home 59 | 1 60 | 61 | setting 62 | 2 63 | 64 | Info 65 | 3 66 | 67 | Clients 68 | 4 69 | 70 | 71 | dark mode 72 | 5 73 | 5 74 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | 2 | # Redis Pro 3 | 4 | [English](./README.md) | 简体中文 5 | 6 | ![Swift5.0+](https://img.shields.io/badge/Swift-5.0%2B-orange.svg?style=flat) 7 | [![release](https://img.shields.io/github/v/release/cmushroom/redis-pro?include_prereleases)](https://github.com/cmushroom/redis-pro/releases) 8 | ![platforms](https://img.shields.io/badge/Platforms-macOS%20-orange.svg?style=flat) 9 | [![Gitter](https://badges.gitter.im/redis-pro/community.svg)](https://gitter.im/redis-pro/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 10 | 11 | ## 简介 12 | * redis-pro 是一款 redis 轻量客户端管理工具, 采用SwiftUI 编写 13 | * 开发过程中借鉴了 [Sequel-Ace](https://github.com/Sequel-Ace/Sequel-Ace)! 和阿里云DMS,Sequel-Ace (前身 Sequel-Pro) 是一个简洁易用的小众 mysql桌面客户端 14 | 15 | ## 安装 16 | * 到release页面下载安装 17 | [下载地址](https://github.com/cmushroom/redis-pro/releases) 18 | * homebrew 19 | ``` 20 | brew install redis-pro 21 | ``` 22 | 23 | 24 | 25 | 26 | ## 平台 27 | 目前只支持 macos (Intel, Apple Silicon) 平台, 后续考虑支持 ipad os 28 | 29 | ## 功能计划(暂定) 30 | - [x] client list and kill 31 | - [x] homebrew install 32 | - [x] slow log 33 | - [x] redis config update 34 | - [x] ssh login 35 | - [x] favorite delete confirm 36 | - [x] TCA 37 | - [x] delete batch 38 | - [ ] terminal 39 | - [ ] ipad os support 40 | - [ ] ssh key support 41 | 42 | 43 | 44 | ## 版本要求 45 | * macos: >= 11.0 46 | * redis: 3.x¹ ... 6.x 47 | 48 | ## 依赖 49 | * RediStack 采用swiftNIO 编写的redis client 50 | * swift-log swift 日志框架, 是上层框架, 需要具体的实现 51 | * Puppy 日志实现, 滚动写入到日志文件 52 | * SwiftyJSON json 转换 53 | 54 | 55 | 56 | ## 应用截图 57 | 登录页 58 | 0 59 | 60 | 首页 61 | 1 62 | 63 | 设置 64 | 2 65 | 66 | Info 67 | 3 68 | 69 | Clients 70 | 4 71 | 72 | 73 | 暗黑模式 74 | 5 75 | 5 76 | 77 | 78 | ## FAQ 79 | * keys 分页数量不匹配 80 | redis scan 命令特性决定, COUNT 选项的作用就是让用户告知迭代命令, 在每次迭代中应该从数据集里返回多少元素。虽然 COUNT 选项只是对增量式迭代命令的一种提示(hint), 但是在大多数情况下, 这种提示都是有效的。少数情况会发生返回数量与COUNT不一致的情况, 多数发生在keys数量不多, 与页大小差距不大的情况 81 | -------------------------------------------------------------------------------- /Tests/RedisBaseTests/RedisBaseTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisClientBaseTest.swift 3 | // Tests 4 | // 5 | // Created by chengpan on 2022/12/3. 6 | // 7 | 8 | import XCTest 9 | 10 | open class RedisBaseTest: XCTestCase { 11 | open var redisHostname: String { 12 | return ProcessInfo.processInfo.environment["REDIS_HOST"] ?? "localhost" 13 | } 14 | 15 | open var redisPort: Int { 16 | return Int(ProcessInfo.processInfo.environment["REDIS_PORT"] ?? "6379")! 17 | } 18 | 19 | open var redisUsername: String? { 20 | return ProcessInfo.processInfo.environment["REDIS_USERNAME"] 21 | } 22 | 23 | open var redisPassword: String? { 24 | return ProcessInfo.processInfo.environment["REDIS_PW"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/RedisBaseTests/RedisClientBaseTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisClientBaseTests.swift 3 | // Tests 4 | // 5 | // Created by chengpan on 2022/12/3. 6 | // 7 | 8 | @testable import redis_pro 9 | import XCTest 10 | import Foundation 11 | import Logging 12 | 13 | open class RedisClientBaseTest: RedisBaseTest { 14 | let logger = Logger(label: "redis-client-test") 15 | 16 | var redisModel: RedisModel! 17 | var redisClient: RediStackClient! 18 | 19 | open override func setUp() { 20 | logger.info("redis client base test setUp...") 21 | self.redisModel = RedisModel(host: redisHostname, port: redisPort, username: redisUsername, password: redisPassword) 22 | redisClient = .init(redisModel) 23 | // let conn = try await redisClient.initConn(host: redisHostname, port: redisPort, username: redisUsername ?? "", pass: redisPassword ?? "", database: 0) 24 | } 25 | 26 | /// Sends a "FLUSHALL" command to Redis to clear it of any data from the previous test, then closes the connection. 27 | /// 28 | /// If any steps fail, a `fatalError` is thrown. 29 | /// 30 | /// See `XCTest.XCTestCase.tearDown()` 31 | open override func tearDown() { 32 | // Task { 33 | // await redisClient.flushDB() 34 | // } 35 | 36 | redisClient.close() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/RedisCommandTests/RedisClentStringTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisClent.swift 3 | // Tests 4 | // 5 | // Created by chengpan on 2022/12/3. 6 | // 7 | 8 | @testable import redis_pro 9 | import XCTest 10 | import Foundation 11 | 12 | class RedisClentStringTest: RedisClientBaseTest { 13 | let key = "redis_client_test_key" 14 | let value = "redis_client_test_value" 15 | 16 | func testSetKey() async { 17 | await redisClient.set(key, value: value) 18 | } 19 | 20 | func testGetKey() async { 21 | await testSetKey() 22 | let r = await redisClient.get(key) 23 | 24 | logger.info("redis client test get key, r: \(r)") 25 | XCTAssertEqual(value, r) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/Store/AppContextStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppContextStoreTests.swift 3 | // Tests 4 | // 5 | // Created by chengpan on 2023/8/5. 6 | // 7 | 8 | @testable import redis_pro 9 | import Foundation 10 | import XCTest 11 | import ComposableArchitecture 12 | 13 | @MainActor 14 | class AppContextStoreTests: StoreBaseTests { 15 | func testShow() async { 16 | let store = TestStore(initialState: AppContextStore.State()) { 17 | AppContextStore() 18 | } withDependencies: { 19 | $0.redisInstance = redisInstance 20 | $0.redisClient = redisClient 21 | } 22 | 23 | await store.send(.show) { 24 | $0.loading = true 25 | $0.loadingCount = 1 26 | } 27 | await store.send(.hide) { 28 | $0.loading = false 29 | $0.loadingCount = 0 30 | } 31 | 32 | await store.send(.show) { 33 | $0.loading = true 34 | $0.loadingCount = 1 35 | } 36 | await store.send(.show) { 37 | $0.loading = true 38 | $0.loadingCount = 2 39 | } 40 | await store.send(.show) { 41 | $0.loading = true 42 | $0.loadingCount = 3 43 | } 44 | await store.send(.hide) { 45 | $0.loading = true 46 | $0.loadingCount = 2 47 | } 48 | await store.send(.show) { 49 | $0.loading = true 50 | $0.loadingCount = 3 51 | } 52 | await store.send(.hide) { 53 | $0.loading = true 54 | $0.loadingCount = 2 55 | } 56 | await store.send(.hide) { 57 | $0.loading = true 58 | $0.loadingCount = 1 59 | } 60 | await store.send(.hide) { 61 | $0.loading = false 62 | $0.loadingCount = 0 63 | } 64 | await store.send(.hide) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/Store/KeysDelStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreTest.swift 3 | // Tests 4 | // 5 | // Created by chengpan on 2023/8/5. 6 | // 7 | 8 | @testable import redis_pro 9 | import Foundation 10 | import XCTest 11 | import ComposableArchitecture 12 | 13 | @MainActor 14 | class KeysDelStoreTests: StoreBaseTests { 15 | func testBasics() async { 16 | let store = TestStore(initialState: RedisKeysStore.State()) { 17 | RedisKeysStore() 18 | } withDependencies: { 19 | $0.redisInstance = redisInstance 20 | $0.redisClient = redisClient 21 | } 22 | 23 | 24 | await redisClient.set("__keys_del_str_1", value: UUID.init().uuidString) 25 | await redisClient.set("__keys_del_str_2", value: UUID.init().uuidString) 26 | await redisClient.set("__keys_del_str_3", value: UUID.init().uuidString) 27 | await redisClient.set("__keys_del_str_4", value: UUID.init().uuidString) 28 | await redisClient.set("__keys_del_str_5", value: UUID.init().uuidString) 29 | await store.send(.search("__keys_del_str_*")) { 30 | $0.pageState = PageStore.State() 31 | } 32 | 33 | XCTAssertEqual(store.state.tableState.datasource.count, 3) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/Store/RedisKeysStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisKeysStoreTest.swift 3 | // Tests 4 | // 5 | // Created by chengpan on 2024/1/1. 6 | // 7 | 8 | @testable import redis_pro 9 | import Foundation 10 | import XCTest 11 | import ComposableArchitecture 12 | 13 | @MainActor 14 | class RedisKeysStoreTests: StoreBaseTests { 15 | func testBasics() async { 16 | let store = TestStore(initialState: RedisKeysStore.State()) { 17 | RedisKeysStore() 18 | } withDependencies: { 19 | $0.redisClient = redisClient 20 | } 21 | 22 | 23 | await redisClient.set("__keys_del_str_1", value: UUID.init().uuidString) 24 | await redisClient.set("__keys_del_str_2", value: UUID.init().uuidString) 25 | await redisClient.set("__keys_del_str_3", value: UUID.init().uuidString) 26 | await redisClient.set("__keys_del_str_4", value: UUID.init().uuidString) 27 | await redisClient.set("__keys_del_str_5", value: UUID.init().uuidString) 28 | await store.send(.search("__keys_del_str_*")) 29 | 30 | // await store.receive(\.setKeys) 31 | 32 | XCTAssertEqual(store.state.tableState.datasource.count, 5) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/Store/StoreBaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreBaseTests.swift 3 | // Tests 4 | // 5 | // Created by chengpan on 2023/8/5. 6 | // 7 | 8 | @testable import redis_pro 9 | import Foundation 10 | import Logging 11 | import XCTest 12 | import ComposableArchitecture 13 | 14 | 15 | class StoreBaseTests: RedisClientBaseTest { 16 | var redisInstance: RedisInstanceModel! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | self.redisInstance = RedisInstanceModel(redisModel: redisModel) 21 | 22 | logger.info("StoreBaseTests setup...") 23 | } 24 | 25 | 26 | func testExample() { 27 | logger.info("test example ...") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tests.swift 3 | // Tests 4 | // 5 | // Created by chengpan on 2022/12/3. 6 | // 7 | 8 | import XCTest 9 | 10 | final class Tests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | // Any test you write for XCTest can be annotated as throws and async. 24 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 25 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | PreviewsEnabled 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | ShowSharedSchemesAutomaticallyEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/xcdebugger/Expressions.xcexplist: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpanwang.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpanwang.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpanwang.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | ShowSharedSchemesAutomaticallyEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chenpanwang.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chenpanwang.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chenpanwang.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/xcuserdata/chengpanwang.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | CustomDump (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 20 13 | 14 | CustomDump (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 21 20 | 21 | CustomDump (Playground).xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 19 27 | 28 | Playground (Playground) 1.xcscheme 29 | 30 | isShown 31 | 32 | orderHint 33 | 5 34 | 35 | Playground (Playground) 2.xcscheme 36 | 37 | isShown 38 | 39 | orderHint 40 | 6 41 | 42 | Playground (Playground).xcscheme 43 | 44 | isShown 45 | 46 | orderHint 47 | 1 48 | 49 | PromiseKit (Playground) 1.xcscheme 50 | 51 | isShown 52 | 53 | orderHint 54 | 3 55 | 56 | PromiseKit (Playground) 2.xcscheme 57 | 58 | isShown 59 | 60 | orderHint 61 | 4 62 | 63 | PromiseKit (Playground) 3.xcscheme 64 | 65 | isShown 66 | 67 | orderHint 68 | 7 69 | 70 | PromiseKit (Playground) 4.xcscheme 71 | 72 | isShown 73 | 74 | orderHint 75 | 8 76 | 77 | PromiseKit (Playground) 5.xcscheme 78 | 79 | isShown 80 | 81 | orderHint 82 | 9 83 | 84 | PromiseKit (Playground).xcscheme 85 | 86 | isShown 87 | 88 | orderHint 89 | 2 90 | 91 | redis-pro.xcscheme_^#shared#^_ 92 | 93 | orderHint 94 | 0 95 | 96 | 97 | SuppressBuildableAutocreation 98 | 99 | 4320AAEF25B6740900A8E214 100 | 101 | primary 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /redis-pro.xcodeproj/xcuserdata/chenpanwang.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | CustomDump (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 20 13 | 14 | CustomDump (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 21 20 | 21 | CustomDump (Playground).xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 19 27 | 28 | Playground (Playground) 1.xcscheme 29 | 30 | isShown 31 | 32 | orderHint 33 | 5 34 | 35 | Playground (Playground) 2.xcscheme 36 | 37 | isShown 38 | 39 | orderHint 40 | 6 41 | 42 | Playground (Playground).xcscheme 43 | 44 | isShown 45 | 46 | orderHint 47 | 4 48 | 49 | PromiseKit (Playground) 1.xcscheme 50 | 51 | isShown 52 | 53 | orderHint 54 | 8 55 | 56 | PromiseKit (Playground) 2.xcscheme 57 | 58 | isShown 59 | 60 | orderHint 61 | 9 62 | 63 | PromiseKit (Playground).xcscheme 64 | 65 | isShown 66 | 67 | orderHint 68 | 7 69 | 70 | redis-pro.xcscheme_^#shared#^_ 71 | 72 | orderHint 73 | 0 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /redis-pro/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/.DS_Store -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "filename" : "redis@32-1.png", 10 | "idiom" : "mac", 11 | "scale" : "2x", 12 | "size" : "16x16" 13 | }, 14 | { 15 | "filename" : "redis@32.png", 16 | "idiom" : "mac", 17 | "scale" : "1x", 18 | "size" : "32x32" 19 | }, 20 | { 21 | "filename" : "redis@64.png", 22 | "idiom" : "mac", 23 | "scale" : "2x", 24 | "size" : "32x32" 25 | }, 26 | { 27 | "filename" : "redis@128.png", 28 | "idiom" : "mac", 29 | "scale" : "1x", 30 | "size" : "128x128" 31 | }, 32 | { 33 | "filename" : "redis@256.png", 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/AppIcon.appiconset/redis@128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Assets.xcassets/AppIcon.appiconset/redis@128.png -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/AppIcon.appiconset/redis@256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Assets.xcassets/AppIcon.appiconset/redis@256.png -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/AppIcon.appiconset/redis@32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Assets.xcassets/AppIcon.appiconset/redis@32-1.png -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/AppIcon.appiconset/redis@32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Assets.xcassets/AppIcon.appiconset/redis@32.png -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/AppIcon.appiconset/redis@64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Assets.xcassets/AppIcon.appiconset/redis@64.png -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/icon-redis.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "redis@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "redis@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "redis@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/icon-redis.imageset/redis@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Assets.xcassets/icon-redis.imageset/redis@1x.png -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/icon-redis.imageset/redis@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Assets.xcassets/icon-redis.imageset/redis@2x.png -------------------------------------------------------------------------------- /redis-pro/Assets.xcassets/icon-redis.imageset/redis@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Assets.xcassets/icon-redis.imageset/redis@3x.png -------------------------------------------------------------------------------- /redis-pro/Command/AbountCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbountCommand.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/12/3. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct AboutCommands: View { 12 | 13 | @Environment(\.openURL) var openURL 14 | var body: some View { 15 | Button("About") { 16 | guard let url = URL(string: "redis-pro://AboutView") else { 17 | return 18 | } 19 | openURL(url) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /redis-pro/Command/CheckUpdateCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckUpdateCommand.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/12/3. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | 11 | struct CheckUpdateCommands: View { 12 | var body: some View { 13 | Button("Check Update") { 14 | VersionManager().checkUpdate(isNoUpgradeHint: true) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /redis-pro/Command/HomeCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeCommand.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/12/3. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | 11 | struct HomeCommands: View { 12 | @Environment(\.openURL) var openURL 13 | 14 | var body: some View { 15 | Button("Home") { 16 | guard let url = URL(string: Const.REPO_URL) else { 17 | return 18 | } 19 | openURL(url) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /redis-pro/Command/RedisProCommands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisProCommands.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/21. 6 | // 7 | 8 | import SwiftUI 9 | import Cocoa 10 | import Logging 11 | import ComposableArchitecture 12 | 13 | struct RedisProCommands: Commands { 14 | 15 | let logger = Logger(label: "commands") 16 | 17 | var body: some Commands { 18 | SidebarCommands() 19 | 20 | CommandGroup(replacing: CommandGroupPlacement.toolbar) { 21 | Button("New Tab", action: { 22 | 23 | if let currentWindow = NSApp.keyWindow, 24 | let windowController = currentWindow.windowController { 25 | windowController.newWindowForTab(nil) 26 | if let newWindow = NSApp.keyWindow, 27 | currentWindow != newWindow { 28 | currentWindow.addTabbedWindow(newWindow, ordered: .above) 29 | } 30 | } 31 | 32 | }) 33 | .keyboardShortcut("t", modifiers: [.command]) 34 | } 35 | 36 | CommandGroup(replacing: CommandGroupPlacement.help) { 37 | CheckUpdateCommands() 38 | HomeCommands() 39 | AboutCommands() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /redis-pro/Common/Assert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Assert.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/3/27. 6 | // 7 | 8 | import Foundation 9 | 10 | class Assert { 11 | static func isTrue(_ bool:Bool, message:String) throws -> Void { 12 | if (!bool) { 13 | try fail(message) 14 | } 15 | } 16 | 17 | static func fail(_ message:String) throws{ 18 | throw BizError(message: message) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /redis-pro/Common/BizError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BizError.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/1/28. 6 | // 7 | 8 | import Foundation 9 | 10 | struct BizError: Error{ 11 | public let message: String 12 | 13 | public var errorDescription: String? { return message } 14 | 15 | init(message:String) { 16 | self.message = message 17 | } 18 | init(_ message:String) { 19 | self.message = message 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /redis-pro/Common/Const.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Const.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/6. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Const { 11 | static let EMPTY_STRING = "" 12 | 13 | static let LIST_VALUE_DELETE_MARK = "__REDIS_LIST_VALUE_DELETE_BY_REDIS_PRO__" 14 | 15 | static let REPO_URL = "https://github.com/cmushroom/redis-pro" 16 | static let RELEASE_URL = "https://github.com/cmushroom/redis-pro/releases" 17 | 18 | // 最大字符串展示长度默认值 19 | static let DEFAULT_STRING_MAX_LENGTH = 10240 20 | } 21 | -------------------------------------------------------------------------------- /redis-pro/Common/DoubleFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleFormatter.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/7. 6 | // 7 | 8 | import Foundation 9 | 10 | public class DoubleFormatter: Formatter { 11 | 12 | override public func string(for obj: Any?) -> String? { 13 | var retVal: String? 14 | let formatter = NumberFormatter() 15 | formatter.numberStyle = .decimal 16 | formatter.hasThousandSeparators = false 17 | formatter.maximumIntegerDigits = 50 18 | formatter.minimumFractionDigits = 0 19 | formatter.maximumFractionDigits = 8 20 | 21 | if let dbl = obj as? Double { 22 | retVal = formatter.string(from: NSNumber(value: dbl)) 23 | } else { 24 | retVal = nil 25 | } 26 | 27 | return retVal 28 | } 29 | 30 | override public func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool { 31 | 32 | var retVal = true 33 | 34 | if let dbl = Double(string), let objok = obj { 35 | objok.pointee = dbl as AnyObject? 36 | retVal = true 37 | } else { 38 | retVal = false 39 | } 40 | 41 | return retVal 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /redis-pro/Common/Enums/ButtonTypeEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonTypeEnum.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/6. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ButtonTypeEnum: String { 11 | case PRIMARY = "primary" 12 | case DEFAULT = "default" 13 | case PLAIN = "plain" 14 | } 15 | -------------------------------------------------------------------------------- /redis-pro/Common/Enums/MainViewTypeEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewTypeEnum.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | enum MainViewTypeEnum{ 12 | case NONE 13 | // 编辑器 14 | case EDITOR 15 | case SYSTEM 16 | } 17 | -------------------------------------------------------------------------------- /redis-pro/Common/Enums/RedisConnectionTypeEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisConnectionType.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/8/6. 6 | // 7 | 8 | import Foundation 9 | 10 | enum RedisConnectionTypeEnum: String,CaseIterable { 11 | case TCP = "tcp" 12 | case SSH = "ssh" 13 | } 14 | -------------------------------------------------------------------------------- /redis-pro/Common/Enums/RedisKeyTypeEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisKeyTypeEnum.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2021/4/4. 6 | // 7 | 8 | import Foundation 9 | 10 | enum RedisKeyTypeEnum: String,CaseIterable { 11 | case NONE = "none" 12 | case STRING = "string" 13 | case HASH = "hash" 14 | case LIST = "list" 15 | case SET = "set" 16 | case ZSET = "zset" 17 | } 18 | -------------------------------------------------------------------------------- /redis-pro/Common/Enums/TableContextMenuEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableContextMenuEnum.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/8/20. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | 11 | enum TableContextMenu: String{ 12 | case DELETE = "Delete" 13 | case EDIT = "Edit" 14 | 15 | // copy 16 | case COPY = "Copy" 17 | case COPY_SCORE = "Copy Score" 18 | case COPY_FIELD = "Copy Field" 19 | case COPY_VALUE = "Copy Value" 20 | 21 | // key list 22 | case RENAME = "Rename" 23 | // client list 24 | case KILL = "Kill" 25 | 26 | var ext: TableContextMenuExt { 27 | switch self { 28 | case .DELETE: 29 | return .init(keyEquivalent: String(Unicode.Scalar(NSBackspaceCharacter)!)) 30 | case .EDIT: 31 | return .init(keyEquivalent: "e") 32 | case .COPY: 33 | return .init(keyEquivalent: "c") 34 | case .COPY_SCORE: 35 | return .init(keyEquivalent: "") 36 | case .COPY_FIELD: 37 | return .init(keyEquivalent: "") 38 | case .COPY_VALUE: 39 | return .init(keyEquivalent: "") 40 | case .RENAME: 41 | return .init(keyEquivalent: "") 42 | case .KILL: 43 | return .init(keyEquivalent: "k") 44 | } 45 | } 46 | } 47 | 48 | struct TableContextMenuExt { 49 | var keyEquivalent: String 50 | } 51 | -------------------------------------------------------------------------------- /redis-pro/Common/Helpers/DateHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateHelper.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/15. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | class DateHelper { 12 | 13 | static let logger = Logger(label: "number-helper") 14 | 15 | private static var dateTimeFormatter:DateFormatter = initDateTimeFormater() 16 | 17 | private static func initDateTimeFormater() -> DateFormatter { 18 | logger.info("初始化 datetime formatter...") 19 | let dateFormatter = DateFormatter() 20 | dateFormatter.timeZone = TimeZone(abbreviation: "GMT+8") //Set timezone that you want 21 | dateFormatter.locale = NSLocale.current 22 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" //Specify your format that you want 23 | return dateFormatter 24 | } 25 | 26 | static func formatDateTime(timestamp:Int) -> String { 27 | let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) 28 | return dateTimeFormatter.string(from: date) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /redis-pro/Common/Helpers/NumberHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberFormatterHelper.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/8. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | class NumberHelper { 12 | static let logger = Logger(label: "number-helper") 13 | 14 | static var doubleFormatter:NumberFormatter = { 15 | logger.info("NumberFormatHelper init double formatter instance ...") 16 | 17 | let formatter = NumberFormatter() 18 | formatter.locale = Locale(identifier: "en_US") 19 | formatter.numberStyle = .decimal 20 | formatter.hasThousandSeparators = false 21 | formatter.maximumIntegerDigits = 20 22 | formatter.minimumFractionDigits = 0 23 | formatter.maximumFractionDigits = 10 24 | formatter.generatesDecimalNumbers = true 25 | formatter.maximumSignificantDigits = 20 26 | return formatter 27 | }() 28 | 29 | 30 | static var intFormatter:NumberFormatter = { 31 | logger.info("NumberFormatHelper init int formatter instance ...") 32 | 33 | let formatter = NumberFormatter() 34 | formatter.locale = Locale(identifier: "en_US") 35 | formatter.numberStyle = .decimal 36 | formatter.hasThousandSeparators = false 37 | formatter.maximumIntegerDigits = 20 38 | formatter.minimumFractionDigits = 0 39 | formatter.maximumFractionDigits = 0 40 | formatter.generatesDecimalNumbers = true 41 | formatter.maximumSignificantDigits = 20 42 | return formatter 43 | }() 44 | 45 | static func formatDouble(_ value:Double?, defaultValue:String? = "-") -> String { 46 | if value == nil { 47 | return defaultValue ?? "-" 48 | } 49 | 50 | let r = doubleFormatter.string(for: NSNumber(value: value!)) 51 | 52 | if r == nil { 53 | return defaultValue ?? "-" 54 | } 55 | return r! 56 | } 57 | 58 | static func toInt(_ value:Any?, defaultValue:Int? = 0) -> Int { 59 | if value == nil { 60 | return defaultValue! 61 | } 62 | 63 | let r = intFormatter.number(from: "\(value!)")?.intValue 64 | 65 | if r == nil { 66 | return defaultValue! 67 | } 68 | return r! 69 | } 70 | 71 | 72 | static func isInt(_ value:String?) -> Bool { 73 | if value == nil { 74 | return false 75 | } 76 | 77 | return Int(value!) != nil 78 | } 79 | 80 | } 81 | 82 | extension Double { 83 | /// Max double value. 84 | static var max: Double { 85 | return Double(greatestFiniteMagnitude) 86 | } 87 | 88 | /// Min double value. 89 | static var min: Double { 90 | return Double(-greatestFiniteMagnitude) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /redis-pro/Common/Helpers/PasteboardHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasteboardHelper.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/8/20. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | 11 | class PasteboardHelper { 12 | 13 | static func copy(_ value: String) { 14 | let pasteboard = NSPasteboard.general 15 | pasteboard.clearContents() 16 | pasteboard.setString(value, forType: .string) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /redis-pro/Common/LoggerFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggerFactory.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/17. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | import Puppy 11 | 12 | class LoggerFactory { 13 | var puppy = Puppy.init() 14 | 15 | init() { 16 | let console = ConsoleLogger("com.cmushroom.redis-pro.console", logFormat: LogFormatter()) 17 | 18 | let fileURL = URL(fileURLWithPath: "./redis-pro.log").absoluteURL 19 | let fileRotation = try! FileRotationLogger("com.cmushroom.redis-pro.file", 20 | fileURL: fileURL, 21 | rotationConfig: RotationConfig( 22 | maxFileSize: 10 * 1024 * 1024, 23 | maxArchivedFilesCount: 5 24 | )) 25 | self.puppy.add(console) 26 | self.puppy.add(fileRotation) 27 | 28 | self.puppy.info("init logger complete...") 29 | } 30 | 31 | func setUp() -> Void { 32 | LoggingSystem.bootstrap { 33 | var handler = PuppyLogHandler(label: $0, puppy: self.puppy) 34 | // Set the logging level. 35 | handler.logLevel = .info 36 | return handler 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /redis-pro/Common/MTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorEnum.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/6. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct MTheme { 12 | // static var PRIMARY:Color = Color(red: 0.1, green: 0.1, blue: 0.1) 13 | static var PRIMARY:Color = Color.gray 14 | 15 | static var CORNER_RADIUS:CGFloat = 2 16 | 17 | static var DIALOG_W:CGFloat = 640 18 | static var DIALOG_H:CGFloat = 400 19 | 20 | // spacing 21 | static var H_SPACING:CGFloat = 6 22 | static var H_SPACING_L:CGFloat = 10 23 | static var V_SPACING:CGFloat = 6 24 | 25 | static var HEADER_PADDING:EdgeInsets = EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0) 26 | 27 | // font size 28 | static var FONT_SIZE_BUTTON:CGFloat = 12 29 | static var FONT_SIZE_BUTTON_ICON:CGFloat = 11 30 | static var FONT_SIZE_BUTTON_S:CGFloat = 10 31 | static var FONT_SIZE_BUTTON_ICON_S:CGFloat = 9 32 | static var FONT_SIZE_BUTTON_L:CGFloat = 14 33 | static var FONT_SIZE_BUTTON_ICON_L:CGFloat = 12 34 | 35 | static var FONT_FOOTER:Font = .footnote 36 | 37 | // table cell null 38 | static var NULL_STRING = "-" 39 | } 40 | -------------------------------------------------------------------------------- /redis-pro/Common/NIO/ReadTimeoutHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadTimeoutHandler.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2024/1/13. 6 | // 7 | 8 | import Logging 9 | import Foundation 10 | import NIO 11 | import NIOConcurrencyHelpers 12 | 13 | class ReadTimeoutHandler: ChannelInboundHandler { 14 | typealias InboundIn = Any 15 | 16 | private let timeout: TimeAmount 17 | private var scheduledTimeout: Scheduled? 18 | 19 | private let logger = Logger(label: "nio-timeout-handler") 20 | 21 | init(timeout: TimeAmount) { 22 | self.timeout = timeout 23 | } 24 | 25 | func channelActive(context: ChannelHandlerContext) { 26 | // 当通道激活时,设置读取超时事件 27 | self.scheduledTimeout = context.eventLoop.scheduleTask(in: self.timeout) { 28 | // 处理读取超时逻辑,例如关闭通道或发出读取超时错误 29 | self.logger.info("NIO read timed out!") 30 | context.close(promise: nil) 31 | } 32 | 33 | // 继续传递通道激活事件 34 | context.fireChannelActive() 35 | } 36 | 37 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 38 | // 当有数据可读时,重置读取超时事件 39 | self.scheduledTimeout?.cancel() 40 | self.scheduledTimeout = context.eventLoop.scheduleTask(in: self.timeout) { 41 | // 处理读取超时逻辑,例如关闭通道或发出读取超时错误 42 | self.logger.info("NIO read timed out!") 43 | context.close(promise: nil) 44 | } 45 | 46 | // 继续传递读取事件 47 | context.fireChannelRead(data) 48 | } 49 | 50 | func channelInactive(context: ChannelHandlerContext) { 51 | // 当通道非活动时,取消已计划的读取超时事件 52 | self.scheduledTimeout?.cancel() 53 | 54 | // 继续传递通道非活动事件 55 | context.fireChannelInactive() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /redis-pro/Common/NIO/ReconnectHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReconnectHandler.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2024/1/13. 6 | // 7 | 8 | import Foundation 9 | import NIO 10 | 11 | class ReconnectHandler: ChannelInboundHandler { 12 | typealias InboundIn = ByteBuffer 13 | 14 | private var reconnectScheduled: Scheduled? 15 | private let reconnectInterval: TimeAmount 16 | 17 | init(reconnectInterval: TimeAmount) { 18 | self.reconnectInterval = reconnectInterval 19 | } 20 | 21 | func channelActive(context: ChannelHandlerContext) { 22 | print("Connection established") 23 | cancelReconnect() 24 | } 25 | 26 | func channelInactive(context: ChannelHandlerContext) { 27 | print("Connection closed. Will attempt to reconnect in \(reconnectInterval)") 28 | 29 | // Schedule a reconnect after the specified interval 30 | reconnectScheduled = context.eventLoop.scheduleTask(in: reconnectInterval) { 31 | self.reconnect(context: context) 32 | } 33 | } 34 | 35 | private func reconnect(context: ChannelHandlerContext) { 36 | print("Attempting to reconnect...") 37 | // let newBootstrap = makeBootstrap(context: context) 38 | // let newChannel = try! newBootstrap.connect(host: "your_server_host", port: your_server_port).wait() 39 | 40 | // Add handlers to the new channel as needed 41 | // ... 42 | 43 | print("Reconnection successful") 44 | context.fireChannelActive() 45 | } 46 | 47 | private func cancelReconnect() { 48 | reconnectScheduled?.cancel() 49 | reconnectScheduled = nil 50 | } 51 | 52 | // private func makeBootstrap(context: ChannelHandlerContext) -> ClientBootstrap { 53 | // // Configure and return a new bootstrap instance 54 | // // ... 55 | // } 56 | } 57 | -------------------------------------------------------------------------------- /redis-pro/Common/NIO/TimeoutHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeoutHandler.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2024/1/13. 6 | // 7 | 8 | import Foundation 9 | import NIO 10 | 11 | class TimeoutHandler: ChannelInboundHandler { 12 | typealias InboundIn = ByteBuffer 13 | 14 | private var scheduledTimeout: Scheduled? 15 | 16 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 17 | // 处理接收到的数据 18 | // ... 19 | // 如果接收到预期的数据,取消超时任务 20 | cancelTimeout() 21 | } 22 | 23 | func errorCaught(context: ChannelHandlerContext, error: Error) { 24 | // 处理错误 25 | // ... 26 | 27 | // 如果发生错误,取消超时任务 28 | cancelTimeout() 29 | context.fireErrorCaught(error) 30 | } 31 | 32 | private func setupTimeout(context: ChannelHandlerContext, timeout: TimeAmount) { 33 | // 如果已经设置了超时任务,取消它 34 | cancelTimeout() 35 | 36 | // 安排一个新的超时任务 37 | scheduledTimeout = context.eventLoop.scheduleTask(in: timeout) { 38 | self.timeoutExpired(context: context) 39 | } 40 | } 41 | 42 | private func cancelTimeout() { 43 | // 取消超时任务 44 | scheduledTimeout?.cancel() 45 | scheduledTimeout = nil 46 | } 47 | 48 | private func timeoutExpired(context: ChannelHandlerContext) { 49 | // 处理超时 50 | // ... 51 | 52 | // 关闭连接或执行其他操作 53 | context.close(promise: nil) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /redis-pro/Common/NetworkMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkMonitor.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2024/1/13. 6 | // 7 | 8 | import Logging 9 | import Foundation 10 | import Network 11 | 12 | class NetworkMonitor { 13 | private let logger = Logger(label: "network-monitor") 14 | private var monitor: NWPathMonitor? 15 | 16 | init() { 17 | monitor = NWPathMonitor() 18 | } 19 | 20 | func startMonitoring(_ callback: @escaping (Bool) async -> Void) { 21 | monitor?.pathUpdateHandler = { path in 22 | if path.usesInterfaceType(.wifi) { 23 | self.logger.info("WiFi status changed") 24 | if path.status == .satisfied { 25 | self.logger.info("WiFi is connected") 26 | Task { 27 | await callback(true) 28 | } 29 | } else { 30 | self.logger.info("WiFi is not connected") 31 | Task { 32 | await callback(false) 33 | } 34 | } 35 | } 36 | } 37 | 38 | let queue = DispatchQueue(label: "WiFiMonitorQueue") 39 | monitor?.start(queue: queue) 40 | } 41 | 42 | func stopMonitoring() { 43 | monitor?.cancel() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /redis-pro/Common/Objects.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Objects.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Objects { 11 | 12 | static func toDic(mirror: Mirror) -> [String: Any] { 13 | var dic: [String: Any] = [:] 14 | for child in mirror.children { 15 | // 如果没有labe就会被抛弃 16 | if let label = child.label { 17 | let propertyMirror = Mirror(reflecting: child.value) 18 | print(propertyMirror) 19 | dic[label] = child.value 20 | } 21 | } 22 | // 添加父类属性 23 | if let superMirror = mirror.superclassMirror { 24 | let superDic = toDic(mirror: superMirror) 25 | for p in superDic { 26 | dic[p.key] = p.value 27 | } 28 | } 29 | return dic 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /redis-pro/Common/PuppyLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogFormatter.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/17. 6 | // 7 | 8 | import Foundation 9 | import Puppy 10 | 11 | final class LogFormatter: LogFormattable { 12 | func formatMessage(_ level: LogLevel, message: String, tag: String, function: String, 13 | file: String, line: UInt, swiftLogInfo: [String : String], 14 | label: String, date: Date, threadID: UInt64) -> String { 15 | let date = dateFormatter(date, withFormatter: .init(), dateFormat: "yyyy-MM-dd'T'HH:mm:ss.SSSZ") 16 | let file = fileName(file) 17 | return "\(date) [\(level.emoji) \(level)] \(file)#L.\(line) \(function) \(message)" 18 | } 19 | } 20 | 21 | final class PuppyFileRotationDelegate: FileRotationLoggerDelegate { 22 | func fileRotationLogger(_ fileRotationLogger: FileRotationLogger, 23 | didArchiveFileURL: URL, toFileURL: URL) { 24 | print("puppy didArchiveFileURL: \(didArchiveFileURL), to file URL: \(toFileURL)") 25 | } 26 | func fileRotationLogger(_ fileRotationLogger: FileRotationLogger, 27 | didRemoveArchivedFileURL: URL) { 28 | print("puppy didRemoveArchivedFileURL: \(didRemoveArchivedFileURL)") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /redis-pro/Common/RedisClient/RediStackClientStream.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RediStackClientStream.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/8/21. 6 | // 7 | 8 | import Foundation 9 | import RediStack 10 | 11 | // MARK: - stream function 12 | // stream 13 | extension RediStackClient { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /redis-pro/Common/RedisClient/RedisClientConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisClientConfig.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/3/6. 6 | // 7 | 8 | import Foundation 9 | import RediStack 10 | 11 | 12 | // MARK: -config 13 | extension RediStackClient { 14 | func getConfigList(_ pattern:String = "*") async -> [RedisConfigItemModel] { 15 | logger.info("get redis config list, pattern: \(pattern)...") 16 | 17 | let command: RedisCommand<[RedisConfigItemModel]> = .configList(pattern) 18 | return await send(command, []) 19 | } 20 | 21 | func configRewrite() async -> Bool { 22 | logger.info("redis config rewrite ...") 23 | let command: RedisCommand = .configRewrite() 24 | return await send(command, false) 25 | 26 | } 27 | 28 | func getConfigOne(key:String) async -> String? { 29 | logger.info("get redis config ...") 30 | let command: RedisCommand = .getConfig(key) 31 | return await send(command) 32 | } 33 | 34 | 35 | func setConfig(key:String, value:String) async -> Bool { 36 | logger.info("set redis config, key: \(key), value: \(value)") 37 | let command: RedisCommand = .setConfig(key, value: value) 38 | return await send(command, false) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /redis-pro/Common/RedisClient/RedisClientLua.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisClientLua.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/7/17. 6 | // 7 | 8 | import Foundation 9 | import RediStack 10 | 11 | // MARK: -lua script 12 | extension RediStackClient { 13 | func eval(_ lua:String) async -> String { 14 | logger.info("lua script eval: \(lua)") 15 | guard lua.count > 3 else { 16 | return "lua script invalid!" 17 | } 18 | begin() 19 | defer { 20 | complete() 21 | } 22 | 23 | do { 24 | let lua = StringHelper.trim(StringHelper.removeStartIgnoreCase(lua, start: "eval")) 25 | if !StringHelper.startWith(lua, start: "'") && !StringHelper.startWith(lua, start: "\"") { 26 | throw BizError("lua script syntax error, demo: \"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}\" 2 key1 key2 arg1 arg2") 27 | } 28 | 29 | 30 | let separator = lua[0] 31 | let scriptLastIndex = lua.lastIndexOf(separator)! 32 | let start = lua.index(lua.startIndex, offsetBy: 1) 33 | let script = String(lua[start.. = RedisCommand(keyword: "EVAL", arguments: respValues, mapValueToResult: { 46 | $0.description 47 | }) 48 | return await send(command, "eval error") 49 | } catch { 50 | handleError(error) 51 | } 52 | 53 | return "eval error" 54 | } 55 | 56 | 57 | 58 | func scriptKill() async -> String { 59 | logger.info("lua script kill") 60 | 61 | 62 | let command:RedisCommand = RedisCommand(keyword: "SCRIPT", arguments: [.init(from: "KILL")], mapValueToResult: { 63 | $0.description 64 | }) 65 | return await send(command, "script kill error") 66 | } 67 | 68 | 69 | 70 | func scriptFlush() async -> Void { 71 | logger.info("lua script flush") 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /redis-pro/Common/RedisClient/RedisClientSSH.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisClientSSH.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/8/6. 6 | // 7 | 8 | import Foundation 9 | import RediStack 10 | import NIO 11 | import NIOSSH 12 | import Logging 13 | 14 | // MARK: -ssh 15 | extension RediStackClient { 16 | 17 | func initSSHConn() async throws -> RedisConnection { 18 | let bindHost = "127.0.0.1" 19 | 20 | let sshTunnel = SSHTunnel(sshHost: self.redisModel.sshHost, sshPort: self.redisModel.sshPort, user: self.redisModel.sshUser, pass: self.redisModel.sshPass, targetHost: self.redisModel.host, targetPort: self.redisModel.port) 21 | let localChannel = try await sshTunnel.openSSHTunnel() 22 | 23 | let localBindPort:Int = localChannel.localAddress?.port ?? 0 24 | self.logger.info("init forwarding server success, local port: \(localBindPort)") 25 | self.sshLocalChannel = localChannel 26 | 27 | return try await initConn(host: bindHost, port: localBindPort, username: self.redisModel.username, pass: self.redisModel.password, database: self.redisModel.database) 28 | } 29 | 30 | 31 | func initSSHPool() async throws -> RedisConnectionPool { 32 | let bindHost = "127.0.0.1" 33 | 34 | let sshTunnel = SSHTunnel(sshHost: self.redisModel.sshHost, sshPort: self.redisModel.sshPort, user: self.redisModel.sshUser, pass: self.redisModel.sshPass, targetHost: self.redisModel.host, targetPort: self.redisModel.port) 35 | let localChannel = try await sshTunnel.openSSHTunnel() 36 | 37 | let localBindPort:Int = localChannel.localAddress?.port ?? 0 38 | self.logger.info("init forwarding server success, local port: \(localBindPort)") 39 | self.sshLocalChannel = localChannel 40 | 41 | return try initPool(host: bindHost, port: localBindPort, username: self.redisModel.username, pass: self.redisModel.password, database: self.redisModel.database) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /redis-pro/Common/RedisClient/RedisClientScan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisClientScan.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/3/6. 6 | // 7 | 8 | import Foundation 9 | 10 | extension RediStackClient { 11 | 12 | func isMatchAll(_ keywords:String?) -> Bool { 13 | guard let keywords = keywords else { 14 | return true 15 | } 16 | return keywords.isEmpty || keywords == "*" || keywords == "**" 17 | } 18 | 19 | func isScan(_ keywords:String) -> Bool { 20 | return keywords.isEmpty || keywords.contains("*") || keywords.contains("?") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /redis-pro/Common/RedisClient/RedisClientSlowLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisClientSlowLog.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/3/6. 6 | // 7 | 8 | import Foundation 9 | import RediStack 10 | 11 | // MARK: -slow log 12 | extension RediStackClient { 13 | func slowLogReset() async -> Bool { 14 | logger.info("slow log reset ...") 15 | let command: RedisCommand = .slowlogReset() 16 | return await send(command, false) 17 | 18 | } 19 | 20 | func slowLogLen() async -> Int { 21 | logger.info("get slow log len ...") 22 | let command: RedisCommand = .slowlogLen() 23 | return await send(command, 0) 24 | } 25 | 26 | func getSlowLog(_ size:Int) async -> [SlowLogModel] { 27 | logger.info("get slow log list ...") 28 | let command: RedisCommand<[SlowLogModel]> = .slowlogList(size) 29 | return await send(command, []) 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /redis-pro/Common/RedisClient/RedisClientSystem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisClientSystem.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/3/6. 6 | // 7 | 8 | import Foundation 9 | import RediStack 10 | 11 | // MARK: - system function 12 | // system 13 | extension RediStackClient { 14 | 15 | func selectDB(_ database: Int) async -> Bool { 16 | self.logger.info("select db: \(database)") 17 | self.redisModel.database = database 18 | 19 | self.connPool?.close() 20 | self.connPool = nil 21 | 22 | let command: RedisCommand = .select(database: database) 23 | let _ = await send(command) 24 | return true 25 | } 26 | 27 | func databases() async -> Int { 28 | 29 | let command: RedisCommand = .databases() 30 | return await send(command, 0) 31 | } 32 | 33 | func dbsize() async -> Int { 34 | 35 | let command: RedisCommand = .dbsize() 36 | return await send(command, 0) 37 | } 38 | 39 | func flushDB() async -> Bool { 40 | let command: RedisCommand = .flushDB() 41 | return await send(command, false) 42 | } 43 | 44 | func clientKill(_ clientModel:ClientModel) async -> Bool { 45 | 46 | let command: RedisCommand = .clientKill(clientModel.addr) 47 | return await send(command, false) 48 | } 49 | 50 | func clientList() async -> [ClientModel] { 51 | 52 | let command: RedisCommand<[ClientModel]> = .clientList() 53 | return await send(command, []) 54 | } 55 | 56 | func info() async -> [RedisInfoModel] { 57 | let command: RedisCommand<[RedisInfoModel]> = .info() 58 | return await send(command, []) 59 | } 60 | 61 | func resetState() async -> Bool { 62 | logger.info("reset state...") 63 | let command: RedisCommand = .resetState() 64 | return await send(command, false) 65 | } 66 | 67 | func ping() async -> Bool { 68 | let command: RedisCommand = .ping() 69 | return await send(command) == "PONG" 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /redis-pro/Common/Settings/ColorSchemeEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorSchemeEnum.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/9. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | enum ColorSchemeEnum:String,CaseIterable{ 12 | case SYSTEM = "system" 13 | case DARK = "dark" 14 | case LIGHT = "light" 15 | } 16 | -------------------------------------------------------------------------------- /redis-pro/Common/Settings/RedisFavoriteDefaultSelectTypeEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisFavoriteDefaultSelectTypeEnum.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/5/2. 6 | // 7 | 8 | import Foundation 9 | 10 | enum RedisFavoriteDefaultSelectTypeEnum:String{ 11 | case LAST = "_last" 12 | case ID = "id" 13 | } 14 | -------------------------------------------------------------------------------- /redis-pro/Common/Stopwatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stopwatch.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/28. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Stopwatch { 11 | private var start:Int64 = 0 12 | 13 | init() { 14 | let timeInterval: TimeInterval = Date().timeIntervalSince1970 15 | start = CLongLong(round(timeInterval*1000)) 16 | } 17 | 18 | static func createStarted() -> Stopwatch { 19 | return Stopwatch() 20 | } 21 | 22 | func elapsedMillis() -> Int64 { 23 | let timeInterval: TimeInterval = Date().timeIntervalSince1970 24 | return CLongLong(round(timeInterval * 1000)) - self.start 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /redis-pro/Common/StringFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringFormatter.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/7. 6 | // 7 | 8 | import Foundation 9 | 10 | public class StringFormatter: Formatter { 11 | 12 | override public func string(for obj: Any?) -> String? { 13 | var retVal: String? 14 | 15 | if let str = obj as? String { 16 | retVal = str.trimmingCharacters(in: .whitespacesAndNewlines) 17 | } else { 18 | retVal = nil 19 | } 20 | 21 | return retVal 22 | } 23 | 24 | override public func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool { 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /redis-pro/Common/UserDefaults/UserDefaultsKeysEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultKeys.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/1/29. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | enum UserDefaulsKeysEnum: String { 12 | case RedisFavoriteListKey = "RedisFavoriteListKey" 13 | case RedisLastUseIdKey = "RedisLastUseIdKey" 14 | // 列表默认选中类型, last:最后一个, id: 上次成功连接的redis id 15 | case RedisFavoriteDefaultSelectType = "User.defaultFavorite" 16 | // color scheme 17 | case AppColorScheme = "App.ColorScheme" 18 | // string editor default max length 19 | case AppStringMaxLength = "App.StringMaxLength" 20 | // keepalive second 21 | case AppKeepalive = "App.Keepalive" 22 | // fast page 23 | case AppFastPage = "App.FastPage" 24 | 25 | } 26 | -------------------------------------------------------------------------------- /redis-pro/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleURLSchemes 23 | 24 | redis-pro 25 | 26 | 27 | 28 | CFBundleVersion 29 | $(CURRENT_PROJECT_VERSION) 30 | LSApplicationCategoryType 31 | public.app-category.developer-tools 32 | LSMinimumSystemVersion 33 | $(MACOSX_DEPLOYMENT_TARGET) 34 | 35 | 36 | -------------------------------------------------------------------------------- /redis-pro/Language/MButton.strings/en.xcloc/Source Contents/redis-pro/Views/Components/en.lproj/MButton.strings: -------------------------------------------------------------------------------- 1 | /* 2 | MButton.strings 3 | redis-pro 4 | 5 | Created by chengpanwang on 2021/6/11. 6 | 7 | */ 8 | "hello" = "HELLO"; 9 | -------------------------------------------------------------------------------- /redis-pro/Language/MButton.strings/en.xcloc/Source Contents/redis-pro/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Bundle name */ 2 | "CFBundleName" = "redis-pro"; 3 | -------------------------------------------------------------------------------- /redis-pro/Language/MButton.strings/en.xcloc/Source Contents/redis-pro/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Language/MButton.strings/en.xcloc/Source Contents/redis-pro/en.lproj/Localizable.strings -------------------------------------------------------------------------------- /redis-pro/Language/MButton.strings/en.xcloc/contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "developmentRegion" : "en", 3 | "project" : "redis-pro.xcodeproj", 4 | "targetLocale" : "en", 5 | "toolInfo" : { 6 | "toolBuildNumber" : "12E262", 7 | "toolID" : "com.apple.dt.xcode", 8 | "toolName" : "Xcode", 9 | "toolVersion" : "12.5" 10 | }, 11 | "version" : "1.0" 12 | } -------------------------------------------------------------------------------- /redis-pro/Language/MButton.strings/zh-Hans.xcloc/Source Contents/redis-pro/Views/Components/en.lproj/MButton.strings: -------------------------------------------------------------------------------- 1 | /* 2 | MButton.strings 3 | redis-pro 4 | 5 | Created by chengpanwang on 2021/6/11. 6 | 7 | */ 8 | "hello" = "HELLO"; 9 | -------------------------------------------------------------------------------- /redis-pro/Language/MButton.strings/zh-Hans.xcloc/Source Contents/redis-pro/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Bundle name */ 2 | "CFBundleName" = "redis-pro"; 3 | -------------------------------------------------------------------------------- /redis-pro/Language/MButton.strings/zh-Hans.xcloc/Source Contents/redis-pro/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Language/MButton.strings/zh-Hans.xcloc/Source Contents/redis-pro/en.lproj/Localizable.strings -------------------------------------------------------------------------------- /redis-pro/Language/MButton.strings/zh-Hans.xcloc/contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "developmentRegion" : "en", 3 | "project" : "redis-pro.xcodeproj", 4 | "targetLocale" : "zh-Hans", 5 | "toolInfo" : { 6 | "toolBuildNumber" : "12E262", 7 | "toolID" : "com.apple.dt.xcode", 8 | "toolName" : "Xcode", 9 | "toolVersion" : "12.5" 10 | }, 11 | "version" : "1.0" 12 | } -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/en.xcloc/Source Contents/redis-pro/Language/en.lproj/Localizble.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizble.strings 3 | redis-pro 4 | 5 | Created by chengpanwang on 2021/6/11. 6 | 7 | */ 8 | 9 | "hello" = "HELLO"; 10 | -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/en.xcloc/Source Contents/redis-pro/Language/en.lproj/RedisInfo.strings: -------------------------------------------------------------------------------- 1 | /* 2 | RedisInfo.strings 3 | redis-pro 4 | 5 | Created by chengpanwang on 2021/6/11. 6 | 7 | */ 8 | -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/en.xcloc/Source Contents/redis-pro/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Bundle name */ 2 | "CFBundleName" = "redis-pro"; 3 | -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/en.xcloc/Source Contents/redis-pro/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Language/redis-pro/en.xcloc/Source Contents/redis-pro/en.lproj/Localizable.strings -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/en.xcloc/contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "developmentRegion" : "en", 3 | "project" : "redis-pro.xcodeproj", 4 | "targetLocale" : "en", 5 | "toolInfo" : { 6 | "toolBuildNumber" : "12E262", 7 | "toolID" : "com.apple.dt.xcode", 8 | "toolName" : "Xcode", 9 | "toolVersion" : "12.5" 10 | }, 11 | "version" : "1.0" 12 | } -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/zh-Hans.xcloc/Source Contents/redis-pro/Language/en.lproj/Localizble.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizble.strings 3 | redis-pro 4 | 5 | Created by chengpanwang on 2021/6/11. 6 | 7 | */ 8 | 9 | "hello" = "HELLO"; 10 | -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/zh-Hans.xcloc/Source Contents/redis-pro/Language/en.lproj/RedisInfo.strings: -------------------------------------------------------------------------------- 1 | /* 2 | RedisInfo.strings 3 | redis-pro 4 | 5 | Created by chengpanwang on 2021/6/11. 6 | 7 | */ 8 | -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/zh-Hans.xcloc/Source Contents/redis-pro/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Bundle name */ 2 | "CFBundleName" = "redis-pro"; 3 | -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/zh-Hans.xcloc/Source Contents/redis-pro/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Language/redis-pro/zh-Hans.xcloc/Source Contents/redis-pro/en.lproj/Localizable.strings -------------------------------------------------------------------------------- /redis-pro/Language/redis-pro/zh-Hans.xcloc/contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "developmentRegion" : "en", 3 | "project" : "redis-pro.xcodeproj", 4 | "targetLocale" : "zh-Hans", 5 | "toolInfo" : { 6 | "toolBuildNumber" : "12E262", 7 | "toolID" : "com.apple.dt.xcode", 8 | "toolName" : "Xcode", 9 | "toolVersion" : "12.5" 10 | }, 11 | "version" : "1.0" 12 | } -------------------------------------------------------------------------------- /redis-pro/Model/ClientModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/18. 6 | // 7 | //"id", "name", "addr", "laddr", "fd", "age", "idle", "flags", "db", "sub", "psub", "multi", "qbuf", "qbuf-free", "obl", "oll", "omem", "events", "cmd", "argv-mem", "tot-mem", "redir", "user" 8 | 9 | import Foundation 10 | 11 | public class ClientModel:NSObject, Identifiable { 12 | @objc public var id:String = "" 13 | @objc var name:String = "" 14 | @objc var addr:String = "" 15 | @objc var laddr:String = "" 16 | @objc var fd:String = "" 17 | @objc var age:String = "" 18 | @objc var idle:String = "" 19 | // O : 客户端是 MONITOR 模式下的附属节点(slave) 20 | // S : 客户端是一般模式下(normal)的附属节点 21 | // M : 客户端是主节点(master) 22 | // x : 客户端正在执行事务 23 | // b : 客户端正在等待阻塞事件 24 | // i : 客户端正在等待 VM I/O 操作(已废弃) 25 | // d : 一个受监视(watched)的键已被修改, EXEC 命令将失败 26 | // c : 在将回复完整地写出之后,关闭链接 27 | // u : 客户端未被阻塞(unblocked) 28 | // A : 尽可能快地关闭连接 29 | // N : 未设置任何 flag 30 | @objc var flags:String = "" 31 | @objc var db:String = "" 32 | @objc var sub:String = "" 33 | @objc var psub:String = "" 34 | @objc var multi:String = "" 35 | @objc var qbuf:String = "" 36 | @objc var qbuf_free:String = "" 37 | @objc var obl:String = "" 38 | @objc var oll:String = "" 39 | @objc var omem:String = "" 40 | // r : 客户端套接字(在事件 loop 中)是可读的(readable) 41 | // w : 客户端套接字(在事件 loop 中)是可写的(writeable) 42 | @objc var events:String = "" 43 | @objc var cmd:String = "" 44 | @objc var argv_mem:String = "" 45 | @objc var tot_mem:String = "" 46 | @objc var redir:String = "" 47 | @objc var user:String = "" 48 | 49 | override init() { 50 | } 51 | 52 | init(line:String) { 53 | let kvStrArray = line.components(separatedBy: .whitespaces) 54 | 55 | var item:[String:String] = [String:String]() 56 | kvStrArray.forEach({kvStr in 57 | if kvStr.contains("=") { 58 | let kv = kvStr.components(separatedBy: "=") 59 | if kv.count == 2 { 60 | item[kv[0]] = kv[1] 61 | } 62 | } 63 | }) 64 | 65 | self.id = item["id"] ?? "" 66 | self.name = item["name"] ?? "" 67 | self.addr = item["addr"] ?? "" 68 | self.laddr = item["laddr"] ?? "" 69 | self.fd = item["fd"] ?? "" 70 | self.age = item["age"] ?? "" 71 | self.idle = item["idle"] ?? "" 72 | self.flags = item["flags"] ?? "" 73 | self.db = item["db"] ?? "" 74 | self.sub = item["sub"] ?? "" 75 | self.psub = item["psub"] ?? "" 76 | self.multi = item["multi"] ?? "" 77 | self.qbuf = item["qbuf"] ?? "" 78 | self.qbuf_free = item["qbuf-free"] ?? "" 79 | self.obl = item["obl"] ?? "" 80 | self.oll = item["oll"] ?? "" 81 | self.omem = item["omem"] ?? "" 82 | self.events = item["events"] ?? "" 83 | self.cmd = item["cmd"] ?? "" 84 | self.argv_mem = item["argv-mem"] ?? "" 85 | self.tot_mem = item["tot-mem"] ?? "" 86 | self.redir = item["redir"] ?? "" 87 | self.user = item["user"] ?? "" 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /redis-pro/Model/GlobalContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalContext.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/15. 6 | // 7 | 8 | import Foundation 9 | import RediStack 10 | 11 | class GlobalContext:ObservableObject, CustomStringConvertible { 12 | @Published var alertVisible:Bool = false 13 | var alertTitle:String = "" 14 | var alertMessage:String = "" 15 | var showSecondButton:Bool = false 16 | var primaryButtonText:String = "Ok" 17 | var secondButtonText:String = "Cancel" 18 | var primaryAction:() throws -> Void = {} 19 | 20 | @Published var loading:Bool = false 21 | 22 | 23 | func showError(_ error:Error) -> Void { 24 | self.alertVisible = true 25 | self.alertTitle = "Error!" 26 | self.primaryButtonText = "Ok" 27 | self.secondButtonText = "Cancel" 28 | 29 | if error is BizError { 30 | alertMessage = (error as! BizError).message 31 | } else if error is RedisError { 32 | alertMessage = (error as! RedisError).message 33 | } else { 34 | alertMessage = "\(error)" 35 | } 36 | } 37 | 38 | func showAlert(_ alertTitle:String, alertMessage:String = "", primaryAction: @escaping () throws -> Void = {}, primaryButtonText:String = "Ok") -> Void { 39 | self.alertVisible = true 40 | self.alertTitle = alertTitle 41 | self.alertMessage = alertMessage 42 | self.primaryAction = primaryAction 43 | self.primaryButtonText = primaryButtonText 44 | } 45 | 46 | func confirm(_ alertTitle:String, alertMessage:String = "", primaryAction: @escaping () throws -> Void = {}, primaryButton:String = "Ok") -> Void { 47 | self.alertVisible = true 48 | self.alertTitle = alertTitle 49 | self.alertMessage = alertMessage 50 | self.primaryAction = primaryAction 51 | self.primaryButtonText = primaryButton 52 | self.showSecondButton = true 53 | } 54 | 55 | var description: String { 56 | return "GlobalContext:[alertVisible:\(alertVisible), loading:\(loading), showSecondButton: \(showSecondButton), primaryButtonText:\(primaryButtonText)]" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /redis-pro/Model/Page.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Page.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/13. 6 | // 7 | 8 | import Foundation 9 | 10 | class Page: CustomStringConvertible, Equatable { 11 | static func == (lhs: Page, rhs: Page) -> Bool { 12 | return lhs.current == rhs.current && lhs.size == rhs.size && lhs.keywords == rhs.keywords 13 | } 14 | 15 | var current: Int = 1 16 | var size: Int = 50 17 | var total: Int = 0 18 | var keywords: String = "" 19 | 20 | var totalPage:Int { 21 | total < 1 ? 1 : (total % size == 0 ? total / size : total / size + 1) 22 | } 23 | 24 | var start: Int { 25 | (self.current - 1 ) * self.size 26 | } 27 | var end: Int { 28 | self.current * self.size 29 | } 30 | 31 | var hasPrev:Bool { 32 | totalPage > 1 && current > 1 33 | } 34 | var hasNext:Bool { 35 | totalPage > 1 && current < totalPage 36 | } 37 | 38 | var description: String { 39 | return "Page:[current:\(current), size:\(size), total:\(total)]" 40 | } 41 | 42 | 43 | func reset() { 44 | self.current = 1 45 | self.total = 0 46 | } 47 | 48 | func firstPage() { 49 | self.current = 1 50 | } 51 | 52 | func nextPage() -> Void { 53 | self.current += 1 54 | } 55 | 56 | func prevPage() -> Void { 57 | if self.current < 2 { 58 | return 59 | } 60 | self.current -= 1 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /redis-pro/Model/RedisConfigItemModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisConfigItemModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public class RedisConfigItemModel:NSObject, Identifiable { 11 | @objc var key:String = "" 12 | @objc var value:String = "" 13 | 14 | override init() { 15 | } 16 | 17 | init(key:String?, value:String?) { 18 | self.key = key ?? "" 19 | self.value = value ?? "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /redis-pro/Model/RedisHashEntryModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisHashEntryModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/16. 6 | // 7 | 8 | import Foundation 9 | 10 | class RedisHashEntryModel:NSObject, Identifiable, ObservableObject { 11 | @objc @Published var field:String = "" 12 | @objc @Published var value:String = "" 13 | @Published var isNew = false 14 | 15 | var id:String { 16 | self.field 17 | } 18 | 19 | override init() { 20 | } 21 | 22 | init(field:String, value:String?) { 23 | self.field = field 24 | self.value = value ?? "" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /redis-pro/Model/RedisInfoItemModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisInfoItemModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public class RedisInfoItemModel: NSObject { 11 | @objc var section:String = "" 12 | @objc var key:String = "" 13 | @objc var value:String = "" 14 | 15 | @objc var desc:String { 16 | let tip:String = NSLocalizedString("REDIS_INFO_\(section)_\(key)".uppercased(), tableName: nil, bundle: Bundle.main, value: "", comment: "") 17 | return tip 18 | } 19 | 20 | override init() { 21 | } 22 | 23 | init(section:String, key:String, value:String) { 24 | self.section = section 25 | self.key = key 26 | self.value = value 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /redis-pro/Model/RedisInfoModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisInfoModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/10. 6 | // 7 | import Foundation 8 | 9 | public class RedisInfoModel:NSObject, Identifiable { 10 | public var id = UUID() 11 | var section:String = "" 12 | var infos:[RedisInfoItemModel] = [RedisInfoItemModel]() 13 | 14 | override init() { 15 | } 16 | 17 | init(section:String) { 18 | self.section = section 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /redis-pro/Model/RedisInsanceModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisInsanceModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/1/25. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | import NIO 11 | import RediStack 12 | import Logging 13 | import ComposableArchitecture 14 | 15 | class RedisInstanceModel: Identifiable { 16 | var redisModel:RedisModel 17 | private var rediStackClient:RediStackClient? 18 | private var appContextviewStore:ViewStoreOf? 19 | private var settingViewStore:ViewStoreOf? 20 | 21 | let logger = Logger(label: "redis-instance") 22 | 23 | 24 | init(redisModel: RedisModel) { 25 | self.redisModel = redisModel 26 | logger.info("redis instance model init") 27 | } 28 | 29 | convenience init(_ redisModel:RedisModel, settingViewStore: ViewStoreOf?) { 30 | self.init(redisModel: redisModel) 31 | self.settingViewStore = settingViewStore 32 | } 33 | 34 | convenience init(_ redisClient: RediStackClient, settingViewStore: ViewStoreOf?) { 35 | self.init(redisModel: redisClient.redisModel) 36 | self.rediStackClient = redisClient 37 | self.settingViewStore = settingViewStore 38 | } 39 | 40 | 41 | func setAppStore(_ appStore: StoreOf) { 42 | let globalStore = appStore.scope(state: \.globalState, action: AppStore.Action.globalAction) 43 | self.appContextviewStore = ViewStore(globalStore, observe: { $0 }) 44 | } 45 | 46 | // get client 47 | func getClient() -> RediStackClient { 48 | if let client = rediStackClient { 49 | return client 50 | } 51 | 52 | return initRedisClient(self.redisModel) 53 | } 54 | 55 | // init redis client 56 | func initRedisClient(_ redisModel: RedisModel) -> RediStackClient { 57 | 58 | logger.info("init new redis client, redisModel: \(redisModel)") 59 | self.redisModel = redisModel 60 | let client = RediStackClient(redisModel, settingViewStore: settingViewStore) 61 | client.setAppContextStore(self.appContextviewStore) 62 | 63 | self.rediStackClient = client 64 | return client 65 | } 66 | 67 | func connect(_ redisModel:RedisModel) async -> Bool { 68 | logger.info("connect to redis server: \(redisModel)") 69 | do { 70 | let r = await testConnect(redisModel) 71 | if r { 72 | let _ = try await initRedisClient(redisModel).getConn() 73 | } 74 | 75 | return r 76 | } catch { 77 | Messages.show(error) 78 | return false 79 | } 80 | } 81 | 82 | func testConnect(_ redisModel:RedisModel) async -> Bool { 83 | defer { 84 | self.close() 85 | } 86 | logger.info("test connect to redis server: \(redisModel)") 87 | return await initRedisClient(redisModel).testConn() 88 | } 89 | 90 | func close() -> Void { 91 | logger.info("redis stack client close...") 92 | rediStackClient?.close() 93 | rediStackClient = nil 94 | } 95 | 96 | func shutdown() -> Void { 97 | rediStackClient?.shutdown() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /redis-pro/Model/RedisKeyModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisKeyModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2021/4/4. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | 11 | class RedisKeyModel: NSObject, Identifiable { 12 | @objc var key: String = "" 13 | @objc var type: String = RedisKeyTypeEnum.STRING.rawValue 14 | var ttl: Int = -1 15 | var isNew: Bool = false 16 | 17 | private var _id:String = "" 18 | var id:String { 19 | if isNew { 20 | return _id 21 | } 22 | return key 23 | } 24 | 25 | convenience init(_ key:String, type:String) { 26 | self.init() 27 | self.key = key 28 | self.type = type 29 | } 30 | 31 | func copyValue(_ redisKeyModel:RedisKeyModel) { 32 | self.isNew = redisKeyModel.isNew 33 | self.key = redisKeyModel.key 34 | self.type = redisKeyModel.type 35 | } 36 | 37 | func initNew() -> Void { 38 | self.isNew = true 39 | self.key = generateKey() 40 | self._id = self.key 41 | self.type = RedisKeyTypeEnum.STRING.rawValue 42 | } 43 | 44 | private func generateKey() -> String { 45 | return "NEW_KEY_\(Date().millis)" 46 | } 47 | 48 | override var description: String { 49 | return "RedisKeyModel:[key:\(key), type:\(type), isNew:\(isNew)]" 50 | } 51 | 52 | public static func == (lhs: RedisKeyModel, rhs: RedisKeyModel) -> Bool { 53 | // 需要比较的值 54 | return lhs.id == rhs.id 55 | } 56 | } 57 | 58 | extension RedisKeyModel { 59 | // type 颜色 60 | @objc var textColor: NSColor { 61 | switch type { 62 | case RedisKeyTypeEnum.STRING.rawValue: 63 | return NSColor.systemBlue 64 | case RedisKeyTypeEnum.HASH.rawValue: 65 | return NSColor.systemPink 66 | case RedisKeyTypeEnum.LIST.rawValue: 67 | return NSColor.systemOrange 68 | case RedisKeyTypeEnum.SET.rawValue: 69 | return NSColor.systemGreen 70 | case RedisKeyTypeEnum.ZSET.rawValue: 71 | return NSColor.systemTeal 72 | default: 73 | return NSColor.systemBrown 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /redis-pro/Model/RedisKeyValueModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisKeyValueModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/7. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct RedisKeyValueModel { 12 | var redisKey:RedisKeyModel 13 | var redisValue:Any 14 | } 15 | 16 | -------------------------------------------------------------------------------- /redis-pro/Model/RedisListItemModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisListItemModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/5/22. 6 | // 7 | 8 | import Foundation 9 | 10 | class RedisListItemModel: NSObject { 11 | 12 | @objc var index:Int = 0 13 | @objc var value:String = "" 14 | 15 | override init() { 16 | } 17 | 18 | init(_ index:Int, _ value:String) { 19 | self.index = index 20 | self.value = value 21 | } 22 | 23 | static func == (lhs: RedisListItemModel, rhs: RedisListItemModel) -> Bool { 24 | return lhs.index == rhs.index && lhs.value == rhs.value 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /redis-pro/Model/RedisZSetItemModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisSetItemModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/16. 6 | // 7 | 8 | import Foundation 9 | 10 | class RedisZSetItemModel:NSObject, ObservableObject, Identifiable { 11 | @objc @Published var value:String = "" 12 | @objc @Published var score:String = "" 13 | 14 | var id:String { 15 | self.value 16 | } 17 | 18 | override init() { 19 | } 20 | 21 | init(value:String, score:String) { 22 | self.score = score 23 | self.value = value 24 | } 25 | 26 | 27 | override var description: String { 28 | return "ZSetItem:[value:\(value), score:\(score)]" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /redis-pro/Model/ScanModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScanModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/1. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | class ScanModel:ObservableObject, CustomStringConvertible, Equatable { 12 | static func == (lhs: ScanModel, rhs: ScanModel) -> Bool { 13 | return lhs.cursor == rhs.cursor && lhs.size == rhs.size && lhs.keywords == rhs.keywords 14 | } 15 | 16 | var current:Int = 1 17 | @Published var total:Int = 0 18 | @Published var cursor:Int = 0 19 | @Published var size:Int = 50 20 | @Published var keywords:String = "" 21 | private var cursorHistory:[Int] = [Int]() 22 | 23 | let logger = Logger(label: "scan-model") 24 | 25 | var totalPage:Int { 26 | if total <= 0 { 27 | return 1 28 | } 29 | 30 | if total % size == 0 { 31 | return total / size 32 | } else { 33 | return total / size + 1 34 | } 35 | } 36 | 37 | var hasNext:Bool { 38 | self.cursor != 0 39 | // self.current < totalPage 40 | } 41 | 42 | var hasPrev:Bool { 43 | self.cursorHistory.count > 0 44 | // self.current > 1 45 | } 46 | 47 | 48 | var description: String { 49 | return "ScanModel:[cursor:\(cursor), size:\(size), keywords:\(keywords), history: \(cursorHistory), current: \(current)]" 50 | } 51 | 52 | func reset() -> Void { 53 | self.current = 1 54 | self.cursor = 0 55 | self.cursorHistory.removeAll() 56 | } 57 | 58 | func nextPage() -> Void { 59 | self.current += 1 60 | self.cursorHistory.append(self.cursor) 61 | } 62 | 63 | func prevPage() -> Void { 64 | self.current -= 1 65 | // 66 | if self.current <= 1 { 67 | self.current = 1 68 | self.cursor = 0 69 | cursorHistory.removeAll() 70 | } 71 | 72 | let index = self.current - 1 73 | self.cursor = (index == 0 || cursorHistory.count == 0) ? 0 : cursorHistory[index - 1] 74 | self.cursorHistory.removeSubrange(index...) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /redis-pro/Model/SlowLogModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlowLogModel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/14. 6 | // 7 | 8 | import Foundation 9 | 10 | public class SlowLogModel:NSObject, Identifiable { 11 | @objc public var id:String = "" 12 | @objc var timestamp:Int = -1 13 | @objc var execTime:String = "" 14 | @objc var cmd:String = "" 15 | @objc var client:String = "" 16 | @objc var clientName:String = "" 17 | 18 | @objc var timestampFormat:String { 19 | timestamp == -1 ? MTheme.NULL_STRING : DateHelper.formatDateTime(timestamp: self.timestamp) 20 | } 21 | 22 | override init() { 23 | } 24 | 25 | init(id:String?, timestamp:Int?, execTime:String?, cmd:String?, client:String?, clientName:String?) { 26 | self.id = id ?? MTheme.NULL_STRING 27 | self.timestamp = timestamp ?? -1 28 | self.execTime = execTime ?? MTheme.NULL_STRING 29 | self.cmd = cmd ?? MTheme.NULL_STRING 30 | self.client = client ?? MTheme.NULL_STRING 31 | self.clientName = clientName ?? MTheme.NULL_STRING 32 | } 33 | 34 | 35 | static func == (lhs: SlowLogModel, rhs: SlowLogModel) -> Bool { 36 | return lhs.id == rhs.id 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /redis-pro/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /redis-pro/Store/AppContextStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalStore.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/6/2. 6 | // 7 | 8 | 9 | import Logging 10 | import Foundation 11 | import ComposableArchitecture 12 | 13 | private let logger = Logger(label: "app-context-store") 14 | 15 | @Reducer 16 | struct AppContextStore { 17 | struct State: Equatable { 18 | var loading:Bool = false 19 | var loadingCount:Int = 0 20 | 21 | init() { 22 | logger.info("app context state init ...") 23 | } 24 | } 25 | 26 | enum Action: Equatable { 27 | case show 28 | case hide 29 | case _hide 30 | } 31 | 32 | var body: some Reducer { 33 | 34 | Reduce { state, action in 35 | switch action { 36 | case .show: 37 | if state.loadingCount <= 0 { 38 | state.loading = true 39 | } 40 | 41 | state.loadingCount += 1 42 | return .none 43 | case .hide: 44 | return .run { send in 45 | try await Task.sleep(nanoseconds: 100_000_000) 46 | await send(._hide) 47 | } 48 | 49 | case ._hide: 50 | state.loadingCount -= 1 51 | if state.loadingCount <= 0 { 52 | state.loading = false 53 | state.loadingCount = 0 54 | } 55 | 56 | return .none 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /redis-pro/Store/DatabaseStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseStore.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2022/5/6. 6 | // 7 | 8 | import Logging 9 | import Foundation 10 | import ComposableArchitecture 11 | 12 | private let logger = Logger(label: "database-store") 13 | 14 | 15 | struct DatabaseStore: Reducer { 16 | struct State: Equatable { 17 | var database: Int = 0 18 | var databases:Int = 16 19 | 20 | init() { 21 | logger.info("database state init ...") 22 | } 23 | } 24 | 25 | 26 | enum Action:BindableAction, Equatable { 27 | case initial 28 | case getDatabases 29 | case setDB(Int) 30 | case selectDB(Int) 31 | case onDBChange(Int) 32 | case none 33 | case binding(BindingAction) 34 | } 35 | 36 | @Dependency(\.redisInstance) var redisInstanceModel:RedisInstanceModel 37 | var mainQueue: AnySchedulerOf = .main 38 | 39 | var body: some Reducer { 40 | BindingReducer() 41 | Reduce { state, action in 42 | switch action { 43 | // 初始化已设置的值 44 | case .initial: 45 | logger.info("database store initial...") 46 | state.database = redisInstanceModel.redisModel.database 47 | return .run { send in 48 | await send(.getDatabases) 49 | } 50 | case .getDatabases: 51 | return .run { send in 52 | let r = await redisInstanceModel.getClient().databases() 53 | await send(.setDB(r)) 54 | } 55 | case let .setDB(databases): 56 | state.databases = databases 57 | return .none 58 | case let .selectDB(database): 59 | state.database = database 60 | 61 | return .run { send in 62 | let r = await redisInstanceModel.getClient().selectDB(database) 63 | if r { 64 | await send(.onDBChange(database)) 65 | } 66 | } 67 | 68 | case .onDBChange: 69 | return .none 70 | case .none: 71 | return .none 72 | case .binding: 73 | return .none 74 | } 75 | } 76 | } 77 | 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /redis-pro/Store/DependencyKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DependencyKeys.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2023/7/2. 6 | // 7 | 8 | import Foundation 9 | import Dependencies 10 | import ComposableArchitecture 11 | 12 | private enum RedisInstanceKey: DependencyKey { 13 | static let liveValue = RedisInstanceModel(redisModel: RedisModel()) 14 | } 15 | 16 | private enum RedisClientKey: DependencyKey { 17 | static let liveValue = RediStackClient(RedisModel()) 18 | } 19 | 20 | /// app 上下文 21 | struct AppContext { 22 | let store: StoreOf 23 | } 24 | private enum AppContextKey: DependencyKey { 25 | static let liveValue = Store(initialState: AppContextStore.State()) { 26 | AppContextStore() 27 | } 28 | } 29 | 30 | 31 | extension DependencyValues { 32 | var redisInstance: RedisInstanceModel { 33 | get { self[RedisInstanceKey.self] } 34 | set { self[RedisInstanceKey.self] = newValue } 35 | } 36 | 37 | var redisClient: RediStackClient { 38 | get { self[RedisClientKey.self] } 39 | set { self[RedisClientKey.self] = newValue } 40 | } 41 | 42 | var appContext: StoreOf { 43 | get { self[AppContextKey.self] } 44 | set { self[AppContextKey.self] = newValue } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /redis-pro/Store/KeyObjectStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectEncodingStore.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2023/7/23. 6 | // 7 | 8 | import Logging 9 | import Foundation 10 | import ComposableArchitecture 11 | 12 | private let logger = Logger(label: "key-object-store") 13 | 14 | struct KeyObjectStore: Reducer { 15 | 16 | struct State: Equatable { 17 | var key: String = "" 18 | var encoding: String = "" 19 | 20 | var redisKeyModel:RedisKeyModel { 21 | get { 22 | let r = RedisKeyModel() 23 | r.key = key 24 | return r 25 | } 26 | set(n) { 27 | key = n.key 28 | } 29 | } 30 | 31 | init() { 32 | logger.info("key object init ...") 33 | } 34 | } 35 | 36 | 37 | enum Action: Equatable { 38 | case initial 39 | case refresh 40 | case setKey(String) 41 | case getEncoding 42 | case setEncoding(String) 43 | } 44 | 45 | @Dependency(\.redisInstance) var redisInstanceModel:RedisInstanceModel 46 | 47 | var body: some Reducer { 48 | Reduce { state, action in 49 | switch action { 50 | // 初始化已设置的值 51 | case .initial: 52 | logger.info("key object initial...") 53 | return .none 54 | 55 | case .refresh: 56 | return .run { send in 57 | await send(.getEncoding) 58 | } 59 | 60 | case let .setKey(key): 61 | state.key = key 62 | return .none 63 | 64 | case .getEncoding: 65 | let key = state.key 66 | return .run { send in 67 | let r = await redisInstanceModel.getClient().objectEncoding(key) 68 | return await send(.setEncoding(r)) 69 | } 70 | 71 | case let .setEncoding(encoding): 72 | state.encoding = encoding 73 | return .none 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /redis-pro/Store/LoadingStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingStore.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/5/3. 6 | // 7 | 8 | 9 | import Logging 10 | import Foundation 11 | import ComposableArchitecture 12 | 13 | private let logger = Logger(label: "loading-store") 14 | 15 | struct LoadingStore: Reducer { 16 | struct State: Equatable { 17 | var loading: Bool = false 18 | 19 | init() { 20 | logger.info("loading state init ...") 21 | } 22 | } 23 | 24 | enum Action:Equatable { 25 | case show 26 | case hide 27 | } 28 | 29 | var body: some Reducer { 30 | 31 | Reduce { state, action in 32 | switch action { 33 | case .show: 34 | state.loading = true 35 | return .none 36 | case .hide: 37 | state.loading = false 38 | return .none 39 | } 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /redis-pro/Store/LuaStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LuaStore.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/7/17. 6 | // 7 | 8 | import Logging 9 | import Foundation 10 | import ComposableArchitecture 11 | 12 | 13 | struct LuaStore: Reducer { 14 | 15 | struct State: Equatable { 16 | @BindingState var lua:String = "\"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}\" 2 key1 key2 arg1 arg2" 17 | @BindingState var evalResult:String = "" 18 | var luaSHA: String = "-" 19 | } 20 | 21 | enum Action:BindableAction,Equatable { 22 | case eval 23 | case scriptKill 24 | case scriptFlush 25 | case scriptLoad 26 | case setLuaResult(String) 27 | case setLuaSHA(String) 28 | case none 29 | case binding(BindingAction) 30 | } 31 | 32 | @Dependency(\.redisInstance) var redisInstanceModel:RedisInstanceModel 33 | let mainQueue: AnySchedulerOf = .main 34 | 35 | var body: some Reducer { 36 | Reduce { state, action in 37 | switch action { 38 | 39 | case .eval: 40 | let lua = state.lua 41 | return .run { send in 42 | let r = await redisInstanceModel.getClient().eval(lua) 43 | await send(.setLuaResult(r)) 44 | } 45 | 46 | case .scriptKill: 47 | 48 | return .run { send in 49 | let _ = await redisInstanceModel.getClient().scriptKill() 50 | } 51 | 52 | case .scriptFlush: 53 | 54 | return .run { send in 55 | let _ = await redisInstanceModel.getClient().scriptFlush() 56 | } 57 | 58 | case .scriptLoad: 59 | 60 | let lua = state.lua 61 | return .run { send in 62 | await send(.setLuaSHA("")) 63 | } 64 | 65 | case let .setLuaResult(r): 66 | state.evalResult = r 67 | return .none 68 | 69 | case let .setLuaSHA(r): 70 | state.luaSHA = r 71 | return .none 72 | 73 | case .none: 74 | return .none 75 | case .binding: 76 | return .none 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /redis-pro/Store/PageStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageStore.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2022/5/6. 6 | // 7 | 8 | import Logging 9 | import Foundation 10 | import ComposableArchitecture 11 | 12 | private let logger = Logger(label: "page-store") 13 | 14 | struct PageStore: Reducer { 15 | struct State: Equatable { 16 | var showTotal: Bool = false 17 | var current:Int = 1 18 | @BindingState var size:Int = 50 19 | var total:Int = 0 20 | var keywords:String = "" 21 | var fastPage = true 22 | var fastPageMax = 99 23 | 24 | /// 总页数 25 | var totalPage:Int { 26 | get { 27 | return total < 1 ? 1 : (total % size == 0 ? total / size : total / size + 1) 28 | } 29 | } 30 | 31 | var totalPageText: String { 32 | get { 33 | if fastPage { 34 | return totalPage > fastPageMax ? "\(fastPageMax)+" : "\(totalPage)" 35 | } 36 | return "\(totalPage)" 37 | } 38 | } 39 | 40 | 41 | var hasPrev:Bool { 42 | totalPage > 1 && current > 1 43 | } 44 | var hasNext:Bool { 45 | totalPage > 1 && current < totalPage 46 | } 47 | 48 | var page:Page { 49 | get { 50 | let page = Page() 51 | page.current = current 52 | page.size = size 53 | page.total = total 54 | page.keywords = keywords 55 | 56 | return page 57 | } 58 | set(page) { 59 | current = page.current 60 | size = page.size 61 | total = page.total 62 | } 63 | } 64 | } 65 | 66 | 67 | enum Action: BindableAction, Equatable { 68 | case binding(BindingAction) 69 | case initial 70 | case updateSize(Int) 71 | case nextPage 72 | case prevPage 73 | case none 74 | } 75 | 76 | 77 | var body: some Reducer { 78 | BindingReducer() 79 | Reduce { state, action in 80 | switch action { 81 | // 初始化已设置的值 82 | case .initial: 83 | logger.info("page store initial...") 84 | return .none 85 | 86 | case let .updateSize(size): 87 | state.current = 1 88 | state.size = size 89 | return .none 90 | case .nextPage: 91 | state.current = state.current + 1 92 | return .none 93 | case .prevPage: 94 | state.current -= 1 95 | if state.current <= 1 { 96 | state.current = 1 97 | } 98 | return .none 99 | case .none: 100 | return .none 101 | case .binding: 102 | return .none 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /redis-pro/Store/RedisSystemStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisSystemStore.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/6/4. 6 | // 7 | 8 | 9 | import Logging 10 | import Foundation 11 | import SwiftyJSON 12 | import ComposableArchitecture 13 | 14 | enum RedisSystemViewTypeEnum{ 15 | case KEYS_DEL 16 | case REDIS_INFO 17 | case REDIS_CONFIG 18 | case CLIENT_LIST 19 | case SLOW_LOG 20 | case LUA 21 | 22 | } 23 | 24 | private let logger = Logger(label: "redis-system-store") 25 | 26 | struct RedisSystemStore: Reducer { 27 | 28 | struct State: Equatable { 29 | var systemView: RedisSystemViewTypeEnum = .REDIS_INFO 30 | var redisInfoState: RedisInfoStore.State = RedisInfoStore.State() 31 | var redisConfigState: RedisConfigStore.State = RedisConfigStore.State() 32 | var slowLogState: SlowLogStore.State = SlowLogStore.State() 33 | var clientListState: ClientListStore.State = ClientListStore.State() 34 | var luaState: LuaStore.State = LuaStore.State() 35 | 36 | init() { 37 | logger.info("redis system state init ...") 38 | } 39 | } 40 | 41 | enum Action: Equatable { 42 | case initial 43 | case setSystemView(RedisSystemViewTypeEnum) 44 | case redisInfoAction(RedisInfoStore.Action) 45 | case redisConfigAction(RedisConfigStore.Action) 46 | case slowLogAction(SlowLogStore.Action) 47 | case clientListAction(ClientListStore.Action) 48 | case luaAction(LuaStore.Action) 49 | } 50 | 51 | @Dependency(\.redisInstance) var redisInstanceModel:RedisInstanceModel 52 | 53 | var body: some Reducer { 54 | Scope(state: \.redisInfoState, action: /Action.redisInfoAction) { 55 | RedisInfoStore() 56 | } 57 | Scope(state: \.redisConfigState, action: /Action.redisConfigAction) { 58 | RedisConfigStore() 59 | } 60 | Scope(state: \.slowLogState, action: /Action.slowLogAction) { 61 | SlowLogStore() 62 | } 63 | Scope(state: \.clientListState, action: /Action.clientListAction) { 64 | ClientListStore() 65 | } 66 | Scope(state: \.luaState, action: /Action.luaAction) { 67 | LuaStore() 68 | } 69 | Reduce { state, action in 70 | switch action { 71 | // 初始化已设置的值 72 | case .initial: 73 | return .none 74 | case let .setSystemView(type): 75 | state.systemView = type 76 | return .none 77 | case .redisInfoAction: 78 | return .none 79 | case .redisConfigAction: 80 | return .none 81 | case .slowLogAction: 82 | return .none 83 | case .clientListAction: 84 | return .none 85 | case .luaAction: 86 | return .none 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /redis-pro/Store/RenameStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RenameStore.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/5/14. 6 | // 7 | 8 | import Logging 9 | import Foundation 10 | import SwiftyJSON 11 | import ComposableArchitecture 12 | 13 | private let logger = Logger(label: "rename-store") 14 | 15 | struct RenameStore: Reducer { 16 | struct State: Equatable { 17 | var key:String = "" 18 | var index:Int = -1 19 | var visible:Bool = false 20 | @BindingState var newKey:String = "" 21 | 22 | init() { 23 | logger.info("string value state init ...") 24 | } 25 | } 26 | 27 | enum Action:BindableAction, Equatable { 28 | case initial 29 | case submit 30 | case setKey(Int, String) 31 | case setNewKey(String) 32 | case hide 33 | case none 34 | case binding(BindingAction) 35 | } 36 | 37 | @Dependency(\.redisInstance) var redisInstanceModel:RedisInstanceModel 38 | var mainQueue: AnySchedulerOf = .main 39 | 40 | var body: some Reducer { 41 | BindingReducer() 42 | Reduce { state, action in 43 | switch action { 44 | // 初始化已设置的值 45 | case .initial: 46 | 47 | logger.info("rename store initial...") 48 | return .none 49 | case .hide: 50 | state.visible = false 51 | return .none 52 | case .submit: 53 | let key = state.key 54 | let index = state.index 55 | let newKey = state.newKey 56 | return .run { send in 57 | 58 | let r = await redisInstanceModel.getClient().rename(key, newKey: newKey) 59 | if r { 60 | await send(.setKey(index, newKey)) 61 | } 62 | } 63 | 64 | case let .setKey(index, newKey): 65 | state.visible = false 66 | return .none 67 | 68 | case let .setNewKey(newKey): 69 | state.newKey = newKey 70 | return .none 71 | case .none: 72 | return .none 73 | 74 | case .binding: 75 | return .none 76 | } 77 | } 78 | } 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /redis-pro/Store/SystemEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemEnvironment.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/6/3. 6 | // 7 | 8 | import Foundation 9 | import ComposableArchitecture 10 | 11 | @dynamicMemberLookup 12 | struct SystemEnvironment { 13 | var date: () -> Date 14 | var environment: Environment 15 | var mainQueue: AnySchedulerOf 16 | var uuid: () -> UUID 17 | 18 | subscript( 19 | dynamicMember keyPath: WritableKeyPath 20 | ) -> Dependency { 21 | get { self.environment[keyPath: keyPath] } 22 | set { self.environment[keyPath: keyPath] = newValue } 23 | } 24 | 25 | /// Creates a live system environment with the wrapped environment provided. 26 | /// 27 | /// - Parameter environment: An environment to be wrapped in the system environment. 28 | /// - Returns: A new system environment. 29 | static func live(environment: Environment) -> Self { 30 | Self( 31 | date: Date.init, 32 | environment: environment, 33 | mainQueue: .main, 34 | uuid: UUID.init 35 | ) 36 | } 37 | 38 | /// Transforms the underlying wrapped environment. 39 | func map( 40 | _ transform: @escaping (Environment) -> NewEnvironment 41 | ) -> SystemEnvironment { 42 | .init( 43 | date: self.date, 44 | environment: transform(self.environment), 45 | mainQueue: self.mainQueue, 46 | uuid: self.uuid 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /redis-pro/Views/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmushroom/redis-pro/8c8645ca6c78c812eb469aec927f4a4f4183bc3a/redis-pro/Views/.DS_Store -------------------------------------------------------------------------------- /redis-pro/Views/App/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/2/12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AboutView: View { 11 | var body: some View { 12 | VStack(spacing: 6) { 13 | Text("Github").font(.title3).bold() 14 | Link("redis-pro", destination: URL(string: "https://github.com/cmushroom/redis-pro")!) 15 | 16 | Text("感谢以下开源项目及项目维护者").font(.title3).bold() 17 | Link("RediStack", destination: URL(string: "https://github.com/Mordil/RediStack")!) 18 | Link("SwiftyJSON", destination: URL(string: "https://github.com/SwiftyJSON/SwiftyJSON")!) 19 | Link("Puppy", destination: URL(string: "https://github.com/sushichop/Puppy")!) 20 | }.padding(40) 21 | .frame(minWidth: 400, maxWidth: 1000, minHeight: 300, maxHeight: 800, alignment: .top) 22 | } 23 | } 24 | 25 | struct AboutView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | AboutView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /redis-pro/Views/App/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/9. 6 | // 7 | 8 | import Logging 9 | import SwiftUI 10 | import ComposableArchitecture 11 | 12 | struct SettingsView: View { 13 | 14 | private let labelWidth:CGFloat = 160 15 | var store:StoreOf 16 | 17 | private let logger = Logger(label: "settings-view") 18 | 19 | var body: some View { 20 | WithViewStore(self.store, observe: { $0 }) { viewStore in 21 | Form { 22 | VStack(alignment: .leading, spacing: 8) { 23 | 24 | Picker(selection: viewStore.binding(get: {$0.defaultFavorite}, send: SettingsStore.Action.setDefaultFavorite), 25 | label: Text("Default Favorite:").frame(width: labelWidth, alignment: .trailing) 26 | ) { 27 | Section { 28 | Text("Last Used").tag("last") 29 | } 30 | 31 | ForEach(viewStore.redisModels, id: \.id) { item in 32 | Text(item.name) 33 | } 34 | } 35 | 36 | Picker(selection: viewStore.binding(get: {$0.colorSchemeValue ?? ColorSchemeEnum.SYSTEM.rawValue}, send: SettingsStore.Action.setColorScheme), 37 | label: Text("Appearance:").frame(width: labelWidth, alignment: .trailing)) { 38 | ForEach(ColorSchemeEnum.allCases.map({$0.rawValue}), id: \.self) { item in 39 | Text(verbatim: item) 40 | } 41 | } 42 | 43 | FormItemInt(label: "String Max Length", labelWidth: labelWidth, tips:"HELP_STRING_GET_RANGE_LENGTH", value: viewStore.binding(get: {$0.stringMaxLength}, send: SettingsStore.Action.setStringMaxLength)) 44 | 45 | Toggle(isOn: viewStore.binding(get: {$0.fastPage}, send: SettingsStore.Action.setFastPage)) { 46 | Text("Fast Page:") 47 | .frame(width: labelWidth, alignment: .trailing) 48 | } 49 | .toggleStyle(.switch) 50 | .help("HELP_FAST_PAGE") 51 | Spacer() 52 | } 53 | } 54 | .onAppear { 55 | viewStore.send(.initial) 56 | } 57 | .navigationTitle("Preferences") 58 | .padding(30) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | struct EdgeBorder: Shape { 12 | 13 | var width: CGFloat 14 | var edges: [Edge] 15 | 16 | func path(in rect: CGRect) -> Path { 17 | var path = Path() 18 | for edge in edges { 19 | var x: CGFloat { 20 | switch edge { 21 | case .top, .bottom, .leading: return rect.minX 22 | case .trailing: return rect.maxX - width 23 | } 24 | } 25 | 26 | var y: CGFloat { 27 | switch edge { 28 | case .top, .leading, .trailing: return rect.minY 29 | case .bottom: return rect.maxY - width 30 | } 31 | } 32 | 33 | var w: CGFloat { 34 | switch edge { 35 | case .top, .bottom: return rect.width 36 | case .leading, .trailing: return self.width 37 | } 38 | } 39 | 40 | var h: CGFloat { 41 | switch edge { 42 | case .top, .bottom: return self.width 43 | case .leading, .trailing: return rect.height 44 | } 45 | } 46 | path.addPath(Path(CGRect(x: x, y: y, width: w, height: h))) 47 | } 48 | return path 49 | } 50 | } 51 | 52 | extension View { 53 | func border(width: CGFloat, edges: [Edge], color: Color) -> some View { 54 | overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color)) 55 | } 56 | } 57 | 58 | extension View { 59 | public func addBorder(_ content: S, width: CGFloat = 1, cornerRadius: CGFloat) -> some View where S : ShapeStyle { 60 | let roundedRect = RoundedRectangle(cornerRadius: cornerRadius) 61 | return clipShape(roundedRect) 62 | .overlay(roundedRect.strokeBorder(content, lineWidth: width)) 63 | } 64 | } 65 | 66 | 67 | extension Date { 68 | 69 | /// 获取当前 秒级 时间戳 - 10位 70 | var timestamp : Int { 71 | let timeInterval: TimeInterval = self.timeIntervalSince1970 72 | return Int(timeInterval) 73 | } 74 | 75 | /// 获取当前 毫秒级 时间戳 - 13位 76 | var millis : Int64 { 77 | let timeInterval: TimeInterval = self.timeIntervalSince1970 78 | return CLongLong(round(timeInterval*1000)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/FormItemDouble.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormItemNumber.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/7. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | struct FormItemDouble: View { 12 | var label:String 13 | var labelWidth:CGFloat = 80 14 | var placeholder:String? 15 | @Binding var value:Double 16 | 17 | let reg = #"^\d+(\.\d+)?$"# 18 | 19 | var body: some View { 20 | // let valueProxy = Binding( 21 | // get: { self.value }, 22 | // set: { 23 | // let result = $0.range( 24 | // of: reg, 25 | // options: .regularExpression 26 | // ) 27 | // 28 | // if result != nil { 29 | // self.value = $0 30 | // } else { 31 | // self.value = "0" 32 | // } 33 | // } 34 | // ) 35 | 36 | HStack(alignment: .center) { 37 | if !label.isEmpty { 38 | FormLabel(label: label, width: labelWidth) 39 | } 40 | MDoubleField(value: $value, placeholder: placeholder) 41 | // MTextField(value: valueProxy) 42 | } 43 | } 44 | } 45 | 46 | //struct FormItemNumber_Previews: PreviewProvider { 47 | // @State static var v:String = "0" 48 | // 49 | // static var previews: some View { 50 | // FormItemDouble(label: "name", value: $v) 51 | // } 52 | //} 53 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/FormItemInt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormItemInt.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/1/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FormItemInt: View { 11 | var label:String 12 | var labelWidth:CGFloat = 80 13 | var placeholder:String? 14 | var tips:LocalizedStringKey? 15 | @Binding var value:Int 16 | var suffix:String? 17 | var onCommit:(() -> Void)? 18 | 19 | 20 | var body: some View { 21 | 22 | HStack(alignment: .center) { 23 | if !label.isEmpty { 24 | FormLabel(label: label, width: labelWidth) 25 | } 26 | // NIntField(value: $value, placeholder: placeholder ?? label, onCommit: onCommit) 27 | // MTextField(value: valueProxy, placeholder: placeholder ?? label, suffix: suffix, onCommit: onCommit, autoCommit: autoCommit) 28 | MIntField(value: $value, placeholder: placeholder ?? label, onCommit: onCommit).help(tips ?? "") 29 | if(tips != nil) { 30 | MIcon(icon: "questionmark.circle", fontSize: 12).help(tips!) 31 | } 32 | } 33 | } 34 | } 35 | 36 | struct FormItemInt_Previews: PreviewProvider { 37 | @State static var v:Int = 0 38 | 39 | static var previews: some View { 40 | FormItemInt(label: "name", value: $v) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/FormItemPassword.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormItemSecure.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FormItemPassword: View { 11 | var label: String 12 | var labelWidth:CGFloat = 80 13 | var required:Bool = false 14 | @Binding var value: String 15 | 16 | var body: some View { 17 | HStack(alignment: .center) { 18 | if !label.isEmpty { 19 | FormLabel(label: label, width: labelWidth, required: required) 20 | } 21 | // MNSTextField(text: $value) 22 | MPasswordField(value: $value) 23 | // NPasswordField(value: $value) 24 | } 25 | } 26 | } 27 | 28 | struct FormItemPassword_Previews: PreviewProvider { 29 | @State static var v: String = ""; 30 | static var previews: some View { 31 | FormItemPassword(label: "aaa", value: $v) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/FormItemText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormItemText.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/1/27. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FormItemText: View { 11 | var label: String? 12 | var labelWidth:CGFloat = 80 13 | var placeholder: String? 14 | var required:Bool = false 15 | var editable:Bool = true 16 | @Binding var value: String 17 | 18 | var body: some View { 19 | HStack(alignment: .center) { 20 | if label != nil && !label!.isEmpty { 21 | FormLabel(label: label!, width: labelWidth, required: required) 22 | } 23 | // NTextField(stringValue: $value, placeholder: placeholder ?? label ?? "") 24 | // MNSTextField(text: $value) 25 | MTextField(value: $value, placeholder: placeholder ?? label, editable: editable) 26 | .frame(maxWidth: .infinity) 27 | } 28 | } 29 | } 30 | 31 | struct FormItemText_Previews: PreviewProvider { 32 | @State static var v: String = ""; 33 | static var previews: some View { 34 | FormItemText(label: "name", placeholder: "please input name", value: $v) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/FormItemTextArea.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormItemTextArea.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FormItemTextArea: View { 11 | var label: String = "" 12 | var labelWidth:CGFloat = 80 13 | var placeholder: String? 14 | var required:Bool = false 15 | @Binding var value: String 16 | 17 | var body: some View { 18 | HStack(alignment: .top) { 19 | if !label.isEmpty { 20 | FormLabel(label: label, width: labelWidth, required: required) 21 | .padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) 22 | } 23 | 24 | MTextEditor(text: $value) 25 | } 26 | } 27 | } 28 | 29 | struct FormItemTextArea_Previews: PreviewProvider { 30 | @State static var v: String = ""; 31 | static var previews: some View { 32 | FormItemTextArea(label: "name", placeholder: "please input name", value: $v) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/FormLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormLabel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/1/27. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FormLabel: View { 11 | var label:String 12 | var width:CGFloat? 13 | var required:Bool = false 14 | 15 | var body: some View { 16 | Text("\(required ? "* " : "")\(label):") 17 | .font(.body) 18 | .lineLimit(1) 19 | .frame(width: width, alignment: .trailing) 20 | 21 | } 22 | } 23 | 24 | struct FormLabel_Previews: PreviewProvider { 25 | static var previews: some View { 26 | FormLabel(label: "form label", width: 80, required: true) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/FormText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormText.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2023/9/10. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FormText: View { 11 | var label: String? 12 | var value: String? 13 | 14 | var body: some View { 15 | HStack { 16 | Text(label ?? "") 17 | .foregroundColor(.secondary) 18 | Text(value ?? "") 19 | .foregroundColor(.primary) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/FormWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormWrapper.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FormWrapper: View { 11 | var body: some View { 12 | Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) 13 | } 14 | } 15 | 16 | struct FormWrapper_Previews: PreviewProvider { 17 | static var previews: some View { 18 | FormWrapper() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/MDoubleField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MDoubleField.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/7. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | 11 | struct MDoubleField: View { 12 | @Binding var value:Double 13 | var placeholder:String? 14 | @State private var isEditing = false 15 | var onCommit:(() -> Void)? 16 | 17 | // 是否有编辑过,编辑过才会触commit 18 | @State private var isEdited:Bool = false 19 | 20 | 21 | let logger = Logger(label: "double-field") 22 | 23 | @ViewBuilder 24 | private var field: some View { 25 | if #available(macOS 12.0, *) { 26 | TextField("", value: $value, formatter: NumberHelper.doubleFormatter, prompt: Text(placeholder ?? "")) 27 | .onSubmit { 28 | doCommit() 29 | } 30 | } else { 31 | TextField(placeholder ?? "", value: $value, formatter: NumberHelper.doubleFormatter, onEditingChanged: { isEditing in 32 | self.isEditing = isEditing 33 | if isEditing { 34 | self.isEdited = true 35 | } 36 | }, onCommit: doCommit) 37 | } 38 | } 39 | 40 | 41 | var body: some View { 42 | HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 2) { 43 | field 44 | .labelsHidden() 45 | .lineLimit(1) 46 | .multilineTextAlignment(.leading) 47 | .font(.body) 48 | .disableAutocorrection(true) 49 | .textFieldStyle(PlainTextFieldStyle()) 50 | .onHover { inside in 51 | self.isEditing = inside 52 | } 53 | } 54 | .padding(EdgeInsets(top: 3, leading: 4, bottom: 3, trailing: 4)) 55 | .background(Color.init(NSColor.textBackgroundColor)) 56 | .cornerRadius(MTheme.CORNER_RADIUS) 57 | .overlay( 58 | RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(isEditing ? 0.4 : 0.2), lineWidth: 1) 59 | ) 60 | } 61 | 62 | func doCommit() -> Void { 63 | if self.isEdited { 64 | self.isEdited = false 65 | logger.info("on double field commit, value: \(value)") 66 | onCommit?() 67 | } 68 | } 69 | } 70 | 71 | struct MDoubleField_Previews: PreviewProvider { 72 | @State static var v:Double = 0 73 | @State static var text:String = "0" 74 | 75 | static var previews: some View { 76 | VStack { 77 | TextField("sfsfdfdf", text: $text) 78 | MDoubleField(value: $v) 79 | Text(String(v)) 80 | Text(text) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/MIntField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MIntField.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2021/12/11. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | 11 | struct MIntField: View { 12 | @Binding var value:Int 13 | var placeholder:String? 14 | @State private var isEditing = false 15 | var onCommit: (() -> Void)? 16 | 17 | // 是否有编辑过,编辑过才会触commit 18 | @State private var isEdited:Bool = false 19 | 20 | let logger = Logger(label: "int-field") 21 | 22 | @ViewBuilder 23 | private var field: some View { 24 | if #available(macOS 12.0, *) { 25 | TextField("", value: $value, formatter: NumberHelper.intFormatter, prompt: Text(placeholder ?? "")) 26 | .onSubmit { 27 | doCommit() 28 | } 29 | } else { 30 | TextField(placeholder ?? "", value: $value, formatter: NumberHelper.intFormatter, onEditingChanged: { isEditing in 31 | self.isEditing = isEditing 32 | if isEditing { 33 | self.isEdited = true 34 | } 35 | }, onCommit: doCommit) 36 | } 37 | } 38 | 39 | var body: some View { 40 | HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 2) { 41 | field 42 | .labelsHidden() 43 | .lineLimit(1) 44 | .multilineTextAlignment(.leading) 45 | .font(.body) 46 | .disableAutocorrection(true) 47 | .textFieldStyle(PlainTextFieldStyle()) 48 | .onHover { inside in 49 | self.isEditing = inside 50 | } 51 | } 52 | .padding(EdgeInsets(top: 3, leading: 4, bottom: 3, trailing: 4)) 53 | .background(Color.init(NSColor.textBackgroundColor)) 54 | .cornerRadius(MTheme.CORNER_RADIUS) 55 | .overlay( 56 | RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(isEditing ? 0.4 : 0.2), lineWidth: 1) 57 | ) 58 | } 59 | 60 | func doCommit() -> Void { 61 | logger.info("on textField commit, value: \(value)") 62 | onCommit?() 63 | } 64 | } 65 | 66 | //struct MIntField_Previews: PreviewProvider { 67 | // static var previews: some View { 68 | // MIntField() 69 | // } 70 | //} 71 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/MSecureField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MSecureField.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | import Logging 12 | 13 | struct MSecureField: View { 14 | @Binding var value:String 15 | var placeholder:String = "Password" 16 | @State private var isEditing = false 17 | var onCommit:() -> Void = {} 18 | 19 | @State private var showPass:Bool = false 20 | 21 | let logger = Logger(label: "secure-field") 22 | 23 | @ViewBuilder 24 | private var field: some View { 25 | if showPass { 26 | MTextField(value: $value, placeholder: placeholder, onCommit: onCommit) 27 | } else { 28 | SecureField(placeholder, text: $value, onCommit: doCommit) 29 | } 30 | } 31 | 32 | var body: some View { 33 | HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 2) { 34 | field 35 | .multilineTextAlignment(/*@START_MENU_TOKEN@*/.leading/*@END_MENU_TOKEN@*/) 36 | .font(.body) 37 | .lineLimit(1) 38 | .disableAutocorrection(true) 39 | .textFieldStyle(PlainTextFieldStyle()) 40 | .onHover { inside in 41 | self.isEditing = inside 42 | } 43 | 44 | MIcon(icon: showPass ? "eye" : "eye.slash", fontSize: MTheme.FONT_SIZE_BUTTON, action: showPassAction) 45 | .padding(0) 46 | } 47 | .padding(EdgeInsets(top: 3, leading: 4, bottom: 3, trailing: 4)) 48 | .background(Color.init(NSColor.textBackgroundColor)) 49 | .cornerRadius(4) 50 | .overlay( 51 | RoundedRectangle(cornerRadius: 4).stroke(Color.gray.opacity(isEditing ? 0.4 : 0.2), lineWidth: 1) 52 | ) 53 | } 54 | 55 | func doCommit() -> Void { 56 | doAction() 57 | } 58 | 59 | func showPassAction() -> Void { 60 | self.showPass.toggle() 61 | } 62 | 63 | func doAction() -> Void { 64 | logger.info("on textField commit, value: \(value)") 65 | onCommit() 66 | } 67 | } 68 | struct MSecureField_Previews: PreviewProvider { 69 | 70 | @State static var text:String = "pass" 71 | static var previews: some View { 72 | MSecureField(value: $text) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/MTextEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTextEditor.swift 3 | // redis-pro 4 | // 存在问题,自动转换双引号, 暂时使用 NSTextEditor 5 | // Created by chengpanwang on 2021/4/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MTextEditor: View { 11 | @Binding var text:String 12 | @State private var editing:Bool = false 13 | @State private var disabled:Bool = false 14 | 15 | var body: some View { 16 | // text editor 17 | TextEditor(text: $text) 18 | .font(.body) 19 | .multilineTextAlignment(.leading) 20 | .padding(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0)) 21 | .lineSpacing(1.5) 22 | .disableAutocorrection(true) 23 | .onHover { inside in 24 | self.editing = inside 25 | } 26 | .addBorder(Color.gray.opacity(!disabled && editing ? 0.4 : 0.2), width: 1, cornerRadius: MTheme.CORNER_RADIUS) 27 | } 28 | } 29 | 30 | 31 | struct MTextEditor_Previews: PreviewProvider { 32 | @State static var text:String = "" 33 | static var previews: some View { 34 | MTextEditor(text: $text) 35 | } 36 | } 37 | 38 | extension NSTextView { 39 | open override var frame: CGRect { 40 | didSet { 41 | self.isAutomaticQuoteSubstitutionEnabled = false 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/NIntField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NIntField.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2021/12/5. 6 | // 7 | 8 | import SwiftUI 9 | import Cocoa 10 | import Logging 11 | 12 | 13 | struct NIntField: NSViewRepresentable { 14 | @Binding var value: Int 15 | var placeholder: String 16 | var disable = false 17 | var onChange: (() -> Void)? 18 | var onCommit: (() -> Void)? 19 | 20 | func makeNSView(context: Context) -> NSTextField { 21 | let textField = NSTextField() 22 | textField.integerValue = value 23 | textField.placeholderString = placeholder 24 | textField.delegate = context.coordinator 25 | 26 | textField.formatter = NumberHelper.intFormatter 27 | textField.isEnabled = !disable 28 | return textField 29 | } 30 | 31 | 32 | func updateNSView(_ nsView: NSTextField, context: Context) { 33 | nsView.integerValue = value 34 | } 35 | 36 | 37 | func makeCoordinator() -> Coordinator { 38 | Coordinator(with: self) 39 | } 40 | 41 | 42 | class Coordinator: NSObject, NSTextFieldDelegate { 43 | let parent: NIntField 44 | private var editing = false 45 | 46 | let logger = Logger(label: "int-field-coordinator") 47 | 48 | init(with parent: NIntField) { 49 | self.parent = parent 50 | super.init() 51 | } 52 | 53 | 54 | // MARK: - NSTextFieldDelegate Methods 55 | 56 | // change 57 | func controlTextDidChange(_ obj: Notification) { 58 | guard let textField = obj.object as? NSTextField else { return } 59 | 60 | if NumberHelper.isInt(textField.stringValue) { 61 | parent.value = textField.integerValue 62 | } else { 63 | textField.stringValue = String(parent.value) 64 | } 65 | editing = true 66 | parent.onChange?() 67 | } 68 | 69 | // commit 70 | func controlTextDidEndEditing(_ obj: Notification) { 71 | if editing { 72 | editing = false 73 | parent.onCommit?() 74 | } 75 | } 76 | 77 | 78 | func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool { 79 | logger.debug("on text field commit, value: \(parent.value)") 80 | parent.onCommit?() 81 | editing = false 82 | return true 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/NPasswordField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NPasswordField.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2021/12/17. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NPasswordField: View { 11 | @Binding var value:String 12 | @State private var visible:Bool = false 13 | 14 | var body: some View { 15 | ZStack(alignment: .trailing) { 16 | if !visible { 17 | // NSecureField(value:$value, placeholder: "Password") 18 | } else { 19 | NTextField(stringValue:$value, placeholder: "Password") 20 | } 21 | Button(action: { 22 | visible.toggle() 23 | }) { 24 | Image(systemName: !self.visible ? "eye.slash" : "eye") 25 | .accentColor(.gray) 26 | } 27 | .onHover { inside in 28 | if inside { 29 | NSCursor.pointingHand.push() 30 | } else { 31 | NSCursor.pop() 32 | } 33 | } 34 | .padding(EdgeInsets(top: 0, leading: 2, bottom: 0, trailing: 2)) 35 | .buttonStyle(PlainButtonStyle()) 36 | .contentShape(Circle()) 37 | } 38 | } 39 | } 40 | 41 | //struct NPasswordField_Previews: PreviewProvider { 42 | // static var previews: some View { 43 | // NPasswordField() 44 | // } 45 | //} 46 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/NSecureField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSecureField.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2021/12/12. 6 | // 7 | import Cocoa 8 | import SwiftUI 9 | import Logging 10 | 11 | struct NSecureField: NSViewRepresentable { 12 | 13 | @Binding var value:String 14 | var placeholder:String? 15 | var onChange: (() -> Void)? 16 | var onCommit: (() -> Void)? 17 | 18 | func makeNSView(context: Context) -> NSSecureTextField { 19 | let textField = NSSecureTextField() 20 | textField.stringValue = value 21 | textField.placeholderString = placeholder 22 | textField.delegate = context.coordinator 23 | // textField.isBordered = true 24 | // textField.bezelStyle = .roundedBezel 25 | return textField 26 | } 27 | 28 | 29 | func updateNSView(_ nsView: NSSecureTextField, context: Context) { 30 | nsView.stringValue = value 31 | } 32 | 33 | func makeCoordinator() -> Coordinator { 34 | Coordinator(with: self) 35 | } 36 | 37 | 38 | class Coordinator: NSObject, NSTextFieldDelegate { 39 | let parent: NSecureField 40 | 41 | 42 | let logger = Logger(label: "text-field-coordinator") 43 | 44 | init(with parent: NSecureField) { 45 | self.parent = parent 46 | super.init() 47 | } 48 | 49 | 50 | // MARK: - NSTextFieldDelegate Methods 51 | 52 | func controlTextDidChange(_ obj: Notification) { 53 | guard let textField = obj.object as? NSSecureTextField else { return } 54 | parent.value = textField.stringValue 55 | parent.onChange?() 56 | } 57 | 58 | func controlTextDidEndEditing(_ obj: Notification) { 59 | guard let textField = obj.object as? NSSecureTextField else { return } 60 | parent.value = textField.stringValue 61 | 62 | parent.onChange?() 63 | } 64 | } 65 | 66 | } 67 | 68 | //struct NSecureField_Previews: PreviewProvider { 69 | // static var previews: some View { 70 | // NSecureField() 71 | // } 72 | //} 73 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Form/NTextEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NTextEditor.swift 3 | // redis-pro 4 | // 5 | // Created by chenpanwang on 2021/12/21. 6 | // 7 | import Cocoa 8 | import SwiftUI 9 | import Logging 10 | 11 | struct NTextEditor: NSViewRepresentable { 12 | @Binding var value: String 13 | // var placeholder: String 14 | var disable = false 15 | var onChange: (() -> Void)? 16 | var onCommit: (() -> Void)? 17 | 18 | func makeNSView(context: Context) -> NSTextView { 19 | let textField = NSTextView() 20 | textField.string = value 21 | // textField.placeho = placeholder 22 | textField.delegate = context.coordinator 23 | 24 | // textField.disable = !disable 25 | return textField 26 | } 27 | 28 | 29 | func updateNSView(_ nsView: NSTextView, context: Context) { 30 | nsView.string = value 31 | } 32 | 33 | 34 | func makeCoordinator() -> Coordinator { 35 | Coordinator(with: self) 36 | } 37 | 38 | 39 | class Coordinator: NSObject, NSTextViewDelegate { 40 | let parent: NTextEditor 41 | private var editing = false 42 | 43 | let logger = Logger(label: "text-editor-coordinator") 44 | 45 | init(with parent: NTextEditor) { 46 | self.parent = parent 47 | super.init() 48 | } 49 | 50 | 51 | // MARK: - NSTextFieldDelegate Methods 52 | 53 | // change 54 | func controlTextDidChange(_ obj: Notification) { 55 | guard let textField = obj.object as? NSTextView else { return } 56 | 57 | parent.value = textField.string 58 | editing = true 59 | parent.onChange?() 60 | } 61 | 62 | // commit 63 | func controlTextDidEndEditing(_ obj: Notification) { 64 | if editing { 65 | editing = false 66 | parent.onCommit?() 67 | } 68 | } 69 | 70 | 71 | func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool { 72 | logger.debug("on text field commit, value: \(parent.value)") 73 | parent.onCommit?() 74 | editing = false 75 | return true 76 | } 77 | } 78 | } 79 | 80 | //struct NTextEditor_Previews: PreviewProvider { 81 | // static var previews: some View { 82 | // NTextEditor() 83 | // } 84 | //} 85 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/IconButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconButton.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/9. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import Cocoa 11 | 12 | struct IconButton: View { 13 | var icon:String 14 | var name:String 15 | var disabled:Bool = false 16 | 17 | var action: (() -> Void) 18 | 19 | let logger = Logger(label: "icon-button") 20 | 21 | var body: some View { 22 | NButton(title: name, action: action, icon: icon, disabled: disabled) 23 | // .buttonStyle(BorderedButtonStyle()) 24 | .disabled(disabled) 25 | .onHover { inside in 26 | if !disabled && inside { 27 | NSCursor.pointingHand.push() 28 | } else { 29 | NSCursor.pop() 30 | } 31 | } 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/KeyObjectBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyObjectBar.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2023/7/30. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct KeyObjectBar: View { 13 | var store:StoreOf 14 | 15 | let logger = Logger(label: "key-object-bar") 16 | 17 | var body: some View { 18 | WithViewStore(self.store, observe: { $0 }) { viewStore in 19 | FormText(label: "Object Encoding:", value: viewStore.encoding) 20 | .padding(EdgeInsets(top: 0, leading: MTheme.H_SPACING, bottom: 0, trailing: MTheme.H_SPACING)) 21 | 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NLoading.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/5/3. 6 | // 7 | 8 | import Logging 9 | import SwiftUI 10 | import ComposableArchitecture 11 | 12 | struct LoadingView: View { 13 | private var logger = Logger(label: "loading-view") 14 | 15 | @Dependency(\.appContext) var appContext 16 | 17 | init() { 18 | logger.info("loading view init...") 19 | } 20 | 21 | var body: some View { 22 | WithViewStore(appContext, observe: { $0 }) {viewStore in 23 | HStack{ 24 | EmptyView() 25 | } 26 | .frame(height: 0) 27 | .overlay(MSpin(loading: viewStore.loading)) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Loadings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loadings.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/6/1. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | 11 | struct Loadings { 12 | static func show() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/MIcon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MIcon.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/7. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MIcon: View { 11 | var icon:String 12 | var fontSize:CGFloat = 10.0 13 | var disabled:Bool = false 14 | var action: () -> Void = {} 15 | 16 | var body: some View { 17 | 18 | Button(action: action) { 19 | Label("", systemImage: icon) 20 | .font(.system(size: fontSize)) 21 | .labelStyle(IconOnlyLabelStyle()) 22 | .frame(height: fontSize) 23 | .contentShape(Circle()) 24 | } 25 | .foregroundColor(.primary) 26 | .contentShape(Circle()) 27 | .buttonStyle(PlainButtonStyle()) 28 | .disabled(disabled) 29 | .onHover { inside in 30 | if !disabled && inside { 31 | NSCursor.pointingHand.push() 32 | } else { 33 | NSCursor.pop() 34 | } 35 | 36 | } 37 | } 38 | } 39 | 40 | struct MIcon_Previews: PreviewProvider { 41 | static var previews: some View { 42 | MIcon(icon: "chevron.right").disabled(true) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/MLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MLabel.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MLabel: View { 11 | var name:String 12 | var icon:String 13 | var size: MLabelSize = .M 14 | 15 | var textSize:CGFloat { 16 | if size == .M { 17 | return MTheme.FONT_SIZE_BUTTON 18 | } else if size == .L { 19 | return MTheme.FONT_SIZE_BUTTON_L 20 | } 21 | 22 | return MTheme.FONT_SIZE_BUTTON_S 23 | } 24 | var iconSize:CGFloat { 25 | if size == .M { 26 | return MTheme.FONT_SIZE_BUTTON_ICON 27 | } else if size == .L { 28 | return MTheme.FONT_SIZE_BUTTON_ICON_L 29 | } 30 | 31 | return MTheme.FONT_SIZE_BUTTON_ICON_S 32 | } 33 | 34 | var body: some View { 35 | Label { 36 | Text(name) 37 | .font(.system(size: textSize)) 38 | } icon: { 39 | Image(systemName: icon) 40 | .font(.system(size: iconSize)) 41 | .padding(EdgeInsets(top: 0, leading: 0, bottom: 2, trailing: -6)) 42 | } 43 | .foregroundColor(.primary) 44 | } 45 | } 46 | 47 | enum MLabelSize { 48 | case S 49 | case M 50 | case L 51 | } 52 | 53 | struct MLabel_Previews: PreviewProvider { 54 | static var previews: some View { 55 | VStack { 56 | MLabel(name: "Add", icon: "plus") 57 | MLabel(name: "DB\(1)", icon: "cylinder.split.1x2", size: .S) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/MLoading.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loading.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MLoading: View { 11 | var text:String = "" 12 | var loadingText:String = "Connecting..." 13 | var loading:Bool = false 14 | var body: some View { 15 | HStack(alignment: .center, spacing: 4) { 16 | if loading { 17 | ProgressView() 18 | .scaleEffect(x: 0.5, y: 0.5, anchor: .center) 19 | .frame(width: 20, height: 20) 20 | } 21 | Text(loading ? loadingText : text) 22 | .font(.body) 23 | .multilineTextAlignment(.leading) 24 | .lineLimit(1) 25 | } 26 | } 27 | } 28 | 29 | struct MLoading_Previews: PreviewProvider { 30 | static var previews: some View { 31 | MLoading() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/MSpin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MSpin.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/21. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | import Foundation 11 | 12 | struct MSpin: View { 13 | var loading:Bool = false 14 | 15 | var spin: some View { 16 | loading ? 17 | VStack(alignment:.center, spacing: 8) { 18 | ProgressView() 19 | Text("Loading...") 20 | } 21 | .padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12)) 22 | .frame(width: 140, height: 120) 23 | .background(Color.black.opacity(0.4)) 24 | .cornerRadius(8) 25 | .shadow(color: .black.opacity(0.6), radius: 8, x: 4, y: 4) 26 | .colorScheme(.dark) 27 | : nil 28 | } 29 | 30 | var body: some View { 31 | spin 32 | } 33 | } 34 | 35 | struct MSpin_Previews: PreviewProvider { 36 | @State static var loading:Bool = true 37 | static var previews: some View { 38 | 39 | HStack { 40 | MSpin(loading: loading) 41 | } 42 | .colorScheme(.dark) 43 | .frame(width: 500, height: 400, alignment: .center) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/MTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTabView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/10. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MTabView: View { 11 | var keys:[String] = ["a", "b", "c"] 12 | @State var selected:Int = 0 13 | 14 | var body: some View { 15 | Picker(selection: $selected, label: Text("Favorite Color")) { 16 | Text("Red").tag(1) 17 | Text("Green").tag(2) 18 | Text("Blue").tag(3) 19 | Text("Other").tag(4) 20 | } 21 | .pickerStyle(SegmentedPickerStyle()) 22 | .horizontalRadioGroupLayout() 23 | } 24 | } 25 | 26 | struct MTabView_Previews: PreviewProvider { 27 | static var previews: some View { 28 | MTabView() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/ModalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModalView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ModalView: View { 11 | @Environment(\.presentationMode) var presentation 12 | var title: String 13 | var action: () throws -> Void 14 | var content: Content 15 | var width = MTheme.DIALOG_W 16 | var height = MTheme.DIALOG_H 17 | 18 | 19 | init(_ title:String, action: @escaping () throws -> Void, @ViewBuilder content: () -> Content) { 20 | self.title = title 21 | self.action = action 22 | self.content = content() 23 | } 24 | 25 | init(_ title:String, width: CGFloat, height: CGFloat, action: @escaping () throws -> Void, @ViewBuilder content: () -> Content) { 26 | self.title = title 27 | self.width = width 28 | self.height = height 29 | self.action = action 30 | self.content = content() 31 | } 32 | 33 | var body: some View { 34 | VStack(alignment: .leading, spacing: 0) { 35 | // header 36 | HStack { 37 | Text(title) 38 | .font(.body) 39 | Spacer() 40 | } 41 | .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) 42 | Rectangle().frame(height: 1) 43 | .padding(.horizontal, 0).foregroundColor(Color.gray.opacity(0.2)) 44 | 45 | VStack(alignment: .center, spacing: 0) { 46 | content 47 | } 48 | .padding(8) 49 | 50 | HStack(alignment: .center, spacing: 6) { 51 | Spacer() 52 | MButton(text: "Cancel", action: onCancel, keyEquivalent: .escape).keyboardShortcut(.cancelAction) 53 | MButton(text: "Submit", action: doAction, keyEquivalent: .return).keyboardShortcut(.defaultAction) 54 | } 55 | .padding(EdgeInsets(top: 0, leading: 8, bottom: 6, trailing: 8)) 56 | } 57 | .frame(minWidth: width, minHeight: height) 58 | .padding(0) 59 | } 60 | 61 | func doAction() -> Void { 62 | presentation.wrappedValue.dismiss() 63 | try? action() 64 | } 65 | 66 | func onCancel() -> Void{ 67 | presentation.wrappedValue.dismiss() 68 | } 69 | } 70 | 71 | struct ModalView_Previews: PreviewProvider { 72 | static var previews: some View { 73 | ModalView("title", action: {print("modal view action")}) { 74 | Text("modal view") 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/PageBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageBar.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/12. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct PageBar: View { 13 | var store:StoreOf 14 | 15 | let logger = Logger(label: "page-bar") 16 | 17 | var body: some View { 18 | WithViewStore(self.store, observe: { $0 }) { viewStore in 19 | 20 | HStack(alignment:.center, spacing: 4) { 21 | if viewStore.showTotal { 22 | Text("Total: \(viewStore.total)") 23 | .font(MTheme.FONT_FOOTER) 24 | .lineLimit(1) 25 | .multilineTextAlignment(.trailing) 26 | } 27 | Picker("", selection: viewStore.$size) { 28 | Text("10").tag(10) 29 | Text("50").tag(50) 30 | Text("100").tag(100) 31 | Text("200").tag(200) 32 | } 33 | .frame(width: 65) 34 | 35 | HStack(alignment:.center, spacing: 2) { 36 | MIcon(icon: "chevron.left", disabled: !viewStore.hasPrev, action: {viewStore.send(.prevPage)}) 37 | Text("\(viewStore.current)/\(viewStore.totalPageText)") 38 | .font(MTheme.FONT_FOOTER) 39 | .multilineTextAlignment(.center) 40 | .lineLimit(1) 41 | .layoutPriority(1) 42 | MIcon(icon: "chevron.right", disabled: !viewStore.hasNext, action: {viewStore.send(.nextPage)}) 43 | } 44 | .frame(minWidth: 60, idealWidth: 60) 45 | } 46 | 47 | } 48 | } 49 | 50 | } 51 | 52 | //struct PageBar_Previews: PreviewProvider { 53 | // static var previews: some View { 54 | // PageBar(page: Page()) 55 | // } 56 | //} 57 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/RedisKeyTypePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisKeyTypePicker.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/7. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RedisKeyTypePicker: View { 11 | var label:String = "" 12 | @Binding var value:String 13 | var disabled:Bool = false 14 | 15 | var body: some View { 16 | Picker("\(label):", selection: $value) { 17 | ForEach(RedisKeyTypeEnum.allCases.filter{$0 != RedisKeyTypeEnum.NONE}, id: \.self) { item in 18 | Text(item.rawValue).tag(item.rawValue) 19 | } 20 | } 21 | .disabled(disabled) 22 | .frame(width: 120) 23 | } 24 | } 25 | 26 | struct RedisKeyTypePicker_Previews: PreviewProvider { 27 | @State static var value = RedisKeyTypeEnum.STRING.rawValue 28 | static var previews: some View { 29 | RedisKeyTypePicker(value: $value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/SearchBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBar.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/12. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct SearchBar: View { 13 | 14 | @State private var keywords: String = "" 15 | var placeholder:String = "Search..." 16 | 17 | var onCommit: ((String) -> Void)? 18 | let logger = Logger(label: "search-bar") 19 | 20 | var body: some View { 21 | 22 | HStack { 23 | // Search text field 24 | NSearchField(value: $keywords, placeholder: placeholder, onCommit: doAction) 25 | .help("HELP_SEARCH_BAR") 26 | } 27 | } 28 | 29 | func doAction(keywords: String) -> Void { 30 | logger.info("on search bar action, keywords: \(keywords)") 31 | onCommit?(keywords) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Table/NTableColumn.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NTableColumn.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/3/27. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import SwiftUI 11 | struct NTableColumn: Equatable { 12 | var type:TableColumnType = .DEFAULT 13 | var title:String 14 | var key:String 15 | var width:CGFloat? 16 | var icon: TableIconEnum? 17 | } 18 | 19 | 20 | extension NSImage.Name { 21 | static let icon = NSImage.Name("icon-redis") 22 | } 23 | 24 | class Icon { 25 | private static func appIcon() -> NSImage { 26 | let icon = NSImage(named: .icon)! 27 | icon.size = NSSize(width: 20, height: 20) 28 | return icon 29 | } 30 | 31 | static var ICON_APP = appIcon() 32 | } 33 | 34 | protocol TableIconImage { 35 | var image:NSImage { get } 36 | } 37 | 38 | 39 | enum TableIconEnum: TableIconImage { 40 | case APP 41 | 42 | var image: NSImage { 43 | switch self { 44 | case .APP: 45 | return Icon.ICON_APP 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Table/TableColumnType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableColumnType.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/3/27. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TableColumnTypeData:Equatable { 11 | var width:CGFloat { get } 12 | } 13 | 14 | 15 | enum TableColumnType: TableColumnTypeData { 16 | 17 | case DEFAULT 18 | case INDEX 19 | case IMAGE 20 | case KEY_TYPE 21 | 22 | // case ID 23 | // case DATE 24 | // case DATETIME 25 | 26 | // 定义每个类型默认宽度 27 | var width: CGFloat { 28 | switch self { 29 | case .DEFAULT: 30 | return 100 31 | case .INDEX: 32 | return 20 33 | case .IMAGE: 34 | return 40 35 | case .KEY_TYPE: 36 | return 80 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/Tag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tag.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/6. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Tag: View { 11 | var name:String 12 | var color:Color = Color.orange 13 | var bgColor:Color = Color.clear 14 | 15 | var body: some View { 16 | Text(name).foregroundColor(color) 17 | .background(bgColor) 18 | } 19 | } 20 | 21 | struct Tag_Previews: PreviewProvider { 22 | static var previews: some View { 23 | Tag(name: "tag") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /redis-pro/Views/Components/TextViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextViewController.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/12. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | import SwiftUI 11 | import Logging 12 | 13 | class TextViewController: NSViewController { 14 | 15 | @objc dynamic var text: String = "" 16 | 17 | @IBOutlet var textView: NSTextView! 18 | @IBOutlet var scrollView: NSScrollView! 19 | @IBOutlet weak var clipView: NSClipView! 20 | 21 | var selectedRanges: [NSValue] = [] { 22 | didSet { 23 | guard selectedRanges.count > 0 else { 24 | return 25 | } 26 | textView.selectedRanges = selectedRanges 27 | } 28 | } 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | self.textView.enabledTextCheckingTypes = 0 33 | } 34 | 35 | func setText(_ text:String) -> Void { 36 | self.text = text 37 | } 38 | } 39 | 40 | 41 | 42 | struct MTextView: NSViewControllerRepresentable { 43 | @Binding var text:String 44 | 45 | let logger = Logger(label: "text-view") 46 | 47 | func makeCoordinator() -> Coordinator { 48 | return Coordinator(self) 49 | } 50 | 51 | 52 | func makeNSViewController(context: Context) -> NSViewController { 53 | let controller = TextViewController() 54 | return controller 55 | } 56 | 57 | 58 | func updateNSViewController(_ nsViewController: NSViewController, context: Context) { 59 | guard let controller = nsViewController as? TextViewController else {return} 60 | controller.textView?.delegate = context.coordinator 61 | controller.setText(text) 62 | controller.selectedRanges = context.coordinator.selectedRanges 63 | } 64 | 65 | class Coordinator: NSObject, NSTextViewDelegate { 66 | 67 | var parent: MTextView 68 | var selectedRanges: [NSValue] = [] 69 | 70 | let logger = Logger(label: "text-view-coordinator") 71 | 72 | 73 | init(_ parent: MTextView) { 74 | self.parent = parent 75 | } 76 | 77 | func textDidChange(_ notification: Notification) { 78 | guard let textView = notification.object as? NSTextView else { return } 79 | self.parent.text = textView.string 80 | self.selectedRanges = textView.selectedRanges 81 | } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /redis-pro/Views/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2021/4/4. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | 13 | struct HomeView: View { 14 | let logger = Logger(label: "home-view") 15 | var store:StoreOf 16 | 17 | var body: some View { 18 | WithViewStore(self.store, observe: { $0.title }) { viewStore in 19 | 20 | RedisKeysListView(store) 21 | .onAppear { 22 | logger.info("redis pro home view init complete") 23 | viewStore.send(.initial) 24 | } 25 | .onDisappear { 26 | logger.info("redis pro home view destroy...") 27 | viewStore.send(.onClose) 28 | } 29 | // 设置window标题 30 | .navigationTitle(viewStore.state) 31 | 32 | } 33 | } 34 | } 35 | 36 | //struct HomeView_Previews: PreviewProvider { 37 | // static var previews: some View { 38 | // HomeView() 39 | // } 40 | //} 41 | -------------------------------------------------------------------------------- /redis-pro/Views/IndexView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/8. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct IndexView: View { 13 | @State var appState:AppStore.State? 14 | var settingStore: StoreOf 15 | let logger = Logger(label: "index-view") 16 | 17 | 18 | init(settingStore: StoreOf) { 19 | logger.info("index view init ...") 20 | self.settingStore = settingStore 21 | } 22 | 23 | var body: some View { 24 | if let state = appState { 25 | let redisInstanceModel = RedisInstanceModel(RedisModel(), settingViewStore: ViewStore(settingStore, observe: { $0 })) 26 | let redisClient = RediStackClient(RedisModel(), settingViewStore: ViewStore(settingStore, observe: { $0 })) 27 | 28 | let store: StoreOf = Store(initialState: state) { 29 | AppStore() 30 | ._printChanges() 31 | } withDependencies: { 32 | $0.redisInstance = redisInstanceModel 33 | $0.redisClient = redisClient 34 | } 35 | 36 | 37 | WithViewStore(store, observe: { $0.isConnect }) {viewStore in 38 | ZStack { 39 | VStack { 40 | if (viewStore.state) { 41 | HomeView(store: store) 42 | } else { 43 | LoginView(store: store) 44 | } 45 | 46 | 47 | 48 | } 49 | 50 | LoadingView() 51 | 52 | }.onAppear { 53 | redisInstanceModel.setAppStore(store) 54 | } 55 | } 56 | 57 | } else { 58 | Spacer() 59 | .onAppear { 60 | appState = AppStore.State() 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /redis-pro/Views/Login/LoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Login.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/1/25. 6 | // 7 | 8 | import SwiftUI 9 | import NIO 10 | import RediStack 11 | import Logging 12 | import ComposableArchitecture 13 | 14 | 15 | struct LoginView: View { 16 | let logger = Logger(label: "login-view") 17 | let store: StoreOf 18 | 19 | init(store: StoreOf) { 20 | logger.info("login view init...") 21 | self.store = store 22 | } 23 | 24 | var body: some View { 25 | RedisListView(store: store.scope(state: \.favoriteState, action: AppStore.Action.favoriteAction)) 26 | .onDisappear { 27 | logger.info("redis pro login view destroy...") 28 | } 29 | .onAppear { 30 | logger.info("redis pro login view init complete") 31 | } 32 | } 33 | } 34 | 35 | //struct Login_Previews: PreviewProvider { 36 | // static var previews: some View { 37 | // LoginView() 38 | // } 39 | //} 40 | -------------------------------------------------------------------------------- /redis-pro/Views/Login/RedisListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisInstanceList.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/1/25. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct RedisListView: View { 13 | let logger = Logger(label: "redis-login") 14 | 15 | var store:StoreOf 16 | 17 | var body: some View { 18 | WithViewStore(self.store, observe: { $0 }) {viewStore in 19 | HSplitView { 20 | VStack(alignment: .leading, 21 | spacing: 0) { 22 | 23 | NTableView( 24 | store: store.scope(state: \.tableState, action: FavoriteStore.Action.tableAction) 25 | ) 26 | 27 | // footer 28 | HStack(alignment: .center) { 29 | MIcon(icon: "plus", fontSize: 13, action: { 30 | viewStore.send(.addNew) 31 | }) 32 | MIcon(icon: "minus", fontSize: 13, disabled: viewStore.tableState.selectIndex < 0, action: { 33 | viewStore.send(.deleteConfirm(viewStore.tableState.selectIndex)) 34 | }) 35 | } 36 | .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)) 37 | } 38 | .padding(0) 39 | .frame(minWidth:200) 40 | .layoutPriority(0) 41 | .onAppear{ 42 | onLoad(viewStore) 43 | } 44 | LoginForm(store: store.scope(state: \.loginState, action: FavoriteStore.Action.loginAction)) 45 | .frame(minWidth: 800, maxWidth: .infinity, minHeight: 520, maxHeight: .infinity) 46 | } 47 | } 48 | } 49 | 50 | func onLoad(_ viewStore:ViewStore) { 51 | viewStore.send(.getAll) 52 | viewStore.send(.initDefaultSelection) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /redis-pro/Views/RedisEditorView/HashEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueRowEditorView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/9. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct HashEditorView: View { 13 | var store: StoreOf 14 | var keyObjectStore: StoreOf 15 | private let logger = Logger(label: "redis-hash-editor") 16 | 17 | init(store: StoreOf) { 18 | self.store = store.scope(state: \.hashValueState, action: ValueStore.Action.hashValueAction) 19 | self.keyObjectStore = store.scope(state: \.keyObjectState, action: ValueStore.Action.keyObjectAction) 20 | } 21 | 22 | 23 | var body: some View { 24 | WithViewStore(self.store, observe: { $0 }) {viewStore in 25 | VStack(alignment: .leading, spacing: 0) { 26 | HStack(alignment: .center , spacing: MTheme.H_SPACING) { 27 | IconButton(icon: "plus", name: "Add", action: {viewStore.send(.addNew)}) 28 | IconButton(icon: "trash", name: "Delete", disabled: viewStore.tableState.selectIndex < 0, action: {viewStore.send(.deleteConfirm(viewStore.tableState.selectIndex))}) 29 | 30 | SearchBar(placeholder: "Search field...", onCommit: {viewStore.send(.search($0))}) 31 | PageBar(store: store.scope(state: \.pageState, action: HashValueStore.Action.pageAction)) 32 | } 33 | .padding(EdgeInsets(top: MTheme.V_SPACING, leading: 0, bottom: MTheme.V_SPACING, trailing: 0)) 34 | 35 | NTableView(store: store.scope(state: \.tableState, action: HashValueStore.Action.tableAction)) 36 | 37 | // footer 38 | HStack(alignment: .center, spacing: 0) { 39 | KeyObjectBar(store: keyObjectStore) 40 | Spacer() 41 | IconButton(icon: "arrow.clockwise", name: "Refresh", action: {viewStore.send(.refresh)}) 42 | 43 | } 44 | .padding(EdgeInsets(top: MTheme.V_SPACING, leading: 0, bottom: 0, trailing: 0)) 45 | } 46 | .onAppear { 47 | logger.info("redis hash editor view appear ...") 48 | viewStore.send(.initial) 49 | } 50 | .sheet(isPresented: viewStore.$editModalVisible, onDismiss: { 51 | }) { 52 | ModalView("Edit hash entry", action: {viewStore.send(.submit)}) { 53 | VStack(alignment:.leading, spacing: 8) { 54 | FormItemText(placeholder: "Field", editable: viewStore.isNew, value: viewStore.$field) 55 | FormItemTextArea(placeholder: "Value", value: viewStore.$value) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /redis-pro/Views/RedisEditorView/ListEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListEditorView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/30. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct ListEditorView: View { 13 | 14 | var store:StoreOf 15 | var keyObjectStore: StoreOf 16 | let logger = Logger(label: "redis-list-editor") 17 | 18 | init(store: StoreOf) { 19 | self.store = store.scope(state: \.listValueState, action: ValueStore.Action.listValueAction) 20 | self.keyObjectStore = store.scope(state: \.keyObjectState, action: ValueStore.Action.keyObjectAction) 21 | } 22 | 23 | var body: some View { 24 | WithViewStore(self.store, observe: { $0 }) { viewStore in 25 | VStack(alignment: .leading, spacing: 0) { 26 | HStack(alignment: .center , spacing: 4) { 27 | IconButton(icon: "plus", name: "Add head", action: { viewStore.send(.addNew(-1))}) 28 | IconButton(icon: "plus", name: "Add tail", action: { viewStore.send(.addNew(-2))}) 29 | IconButton(icon: "trash", name: "Delete", disabled: viewStore.tableState.selectIndex < 0, action: {viewStore.send(.deleteConfirm(viewStore.tableState.selectIndex))}) 30 | 31 | Spacer() 32 | PageBar(store: store.scope(state: \.pageState, action: ListValueStore.Action.pageAction)) 33 | } 34 | .padding(EdgeInsets(top: MTheme.V_SPACING, leading: 0, bottom: MTheme.V_SPACING, trailing: 0)) 35 | 36 | 37 | NTableView(store: store.scope(state: \.tableState, action: ListValueStore.Action.tableAction)) 38 | 39 | // footer 40 | HStack(alignment: .center, spacing: MTheme.H_SPACING) { 41 | KeyObjectBar(store: keyObjectStore) 42 | Spacer() 43 | IconButton(icon: "arrow.clockwise", name: "Refresh", action: {viewStore.send(.refresh)}) 44 | } 45 | .padding(EdgeInsets(top: MTheme.V_SPACING, leading: 0, bottom: 0, trailing: 0)) 46 | } 47 | .sheet(isPresented: viewStore.$editModalVisible, onDismiss: { 48 | }) { 49 | ModalView("Edit list item", action: {viewStore.send(.submit)}) { 50 | VStack(alignment:.leading, spacing: MTheme.V_SPACING) { 51 | FormItemTextArea(label: "", placeholder: "value", value: viewStore.$editValue) 52 | } 53 | 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | //struct ListEditorView_Previews: PreviewProvider { 61 | // static var redisKeyModel:RedisKeyModel = RedisKeyModel(key: "tes", type: "string") 62 | // static var previews: some View { 63 | // ListEditorView(redisKeyModel: redisKeyModel) 64 | // } 65 | //} 66 | -------------------------------------------------------------------------------- /redis-pro/Views/RedisEditorView/RedisValueEditView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisValueEditView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/7. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct RedisValueEditView: View { 13 | 14 | var store: StoreOf 15 | 16 | let logger = Logger(label: "redis-value-edit-view") 17 | 18 | var body: some View { 19 | WithViewStore(self.store, observe: { $0.keyState }) { viewStore in 20 | VStack(alignment: .leading, spacing: 0) { 21 | if viewStore.type == RedisKeyTypeEnum.STRING.rawValue { 22 | StringEditorView(store: store) 23 | } 24 | // HASH 25 | else if viewStore.type == RedisKeyTypeEnum.HASH.rawValue { 26 | HashEditorView(store: store) 27 | } 28 | // LIST 29 | else if viewStore.type == RedisKeyTypeEnum.LIST.rawValue { 30 | ListEditorView(store: store) 31 | } 32 | // SET 33 | else if viewStore.type == RedisKeyTypeEnum.SET.rawValue { 34 | SetEditorView(store: store) 35 | } 36 | // ZSET 37 | else if viewStore.type == RedisKeyTypeEnum.ZSET.rawValue { 38 | ZSetEditorView(store: store) 39 | } else { 40 | EmptyView() 41 | } 42 | } 43 | } 44 | 45 | } 46 | 47 | } 48 | 49 | //struct RedisValueEditView_Previews: PreviewProvider { 50 | // static var previews: some View { 51 | // RedisValueEditView(redisKeyModel: RedisKeyModel(key: "user_session:1234", type: RedisKeyTypeEnum.STRING.rawValue)) 52 | // } 53 | //} 54 | -------------------------------------------------------------------------------- /redis-pro/Views/RedisEditorView/RedisValueHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisValueHeaderView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/7. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct RedisValueHeaderView: View { 13 | 14 | var store: StoreOf 15 | let logger = Logger(label: "redis-value-header") 16 | 17 | private func ttlView(_ viewStore: ViewStore) -> some View { 18 | HStack(alignment:.center, spacing: 0) { 19 | FormItemInt(label: "TTL(s)", value: viewStore.binding(get: \.ttl, send: KeyStore.Action.setTtl), suffix: "square.and.pencil", onCommit: { viewStore.send(.saveTtl)}) 20 | .disabled(viewStore.isNew) 21 | .help("HELP_TTL") 22 | .frame(width: 260) 23 | } 24 | } 25 | 26 | var body: some View { 27 | WithViewStore(self.store, observe: { $0 }) {viewStore in 28 | 29 | HStack(alignment: .center, spacing: 6) { 30 | FormItemText(label: "Key", labelWidth: 40, required: true, editable: viewStore.isNew, value: viewStore.binding(get: \.key, send: KeyStore.Action.setKey)) 31 | .frame(maxWidth: .infinity) 32 | 33 | Spacer() 34 | RedisKeyTypePicker(label: "Type", value: viewStore.binding(get: \.type, send: KeyStore.Action.setType), disabled: !viewStore.isNew) 35 | ttlView(viewStore) 36 | } 37 | } 38 | } 39 | 40 | } 41 | 42 | //struct RedisValueHeaderView_Previews: PreviewProvider { 43 | // static var previews: some View { 44 | // RedisValueHeaderView(redisKeyModel: RedisKeyModel(key: "test", type: RedisKeyTypeEnum.STRING.rawValue)) 45 | // } 46 | //} 47 | -------------------------------------------------------------------------------- /redis-pro/Views/RedisEditorView/RedisValueView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisValueView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/7. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | 11 | struct RedisValueView: View { 12 | var store: StoreOf 13 | 14 | var body: some View { 15 | VStack(alignment: .leading, spacing: 0) { 16 | RedisValueHeaderView(store: store.scope(state: \.keyState, action: ValueStore.Action.keyAction)) 17 | .padding(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) 18 | Rectangle().frame(height: 1) 19 | .padding(.horizontal, 0).foregroundColor(Color.gray.opacity(0.1)) 20 | 21 | RedisValueEditView(store: store) 22 | } 23 | } 24 | } 25 | 26 | //struct RedisValueView_Previews: PreviewProvider { 27 | // @State private static var redisKeyModel = RedisKeyModel("test", type: RedisKeyTypeEnum.STRING.rawValue) 28 | // static var previews: some View { 29 | // RedisValueView(redisKeyModel: $redisKeyModel) 30 | // } 31 | //} 32 | -------------------------------------------------------------------------------- /redis-pro/Views/RedisEditorView/SetEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListEditorView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/30. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct SetEditorView: View { 13 | 14 | var store:StoreOf 15 | var keyObjectStore: StoreOf 16 | let logger = Logger(label: "redis-set-editor") 17 | 18 | init(store: StoreOf) { 19 | self.store = store.scope(state: \.setValueState, action: ValueStore.Action.setValueAction) 20 | self.keyObjectStore = store.scope(state: \.keyObjectState, action: ValueStore.Action.keyObjectAction) 21 | } 22 | 23 | var body: some View { 24 | WithViewStore(self.store, observe: { $0 }) { viewStore in 25 | 26 | VStack(alignment: .leading, spacing: 0) { 27 | HStack(alignment: .center , spacing: 4) { 28 | IconButton(icon: "plus", name: "Add", action: {viewStore.send(.addNew)}) 29 | IconButton(icon: "trash", name: "Delete", disabled: viewStore.tableState.selectIndex < 0, action: {viewStore.send(.deleteConfirm(viewStore.tableState.selectIndex))}) 30 | 31 | SearchBar(placeholder: "Search element...", onCommit: {viewStore.send(.search($0))}) 32 | PageBar(store: store.scope(state: \.pageState, action: SetValueStore.Action.pageAction)) 33 | } 34 | .padding(EdgeInsets(top: MTheme.V_SPACING, leading: 0, bottom: MTheme.V_SPACING, trailing: 0)) 35 | 36 | NTableView(store: store.scope(state: \.tableState, action: SetValueStore.Action.tableAction)) 37 | 38 | 39 | // footer 40 | HStack(alignment: .center, spacing: MTheme.H_SPACING) { 41 | KeyObjectBar(store: keyObjectStore) 42 | Spacer() 43 | IconButton(icon: "arrow.clockwise", name: "Refresh", action: {viewStore.send(.refresh)}) 44 | } 45 | .padding(EdgeInsets(top: MTheme.V_SPACING, leading: 0, bottom: 0, trailing: 0)) 46 | } 47 | .sheet(isPresented: viewStore.$editModalVisible, onDismiss: { 48 | }) { 49 | ModalView("Edit set element", action: {viewStore.send(.submit)}) { 50 | VStack(alignment:.leading, spacing: MTheme.V_SPACING) { 51 | FormItemTextArea(placeholder: "value", value: viewStore.$editValue) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | //struct SetEditorView_Previews: PreviewProvider { 60 | // static var redisKeyModel:RedisKeyModel = RedisKeyModel(key: "tes", type: "string") 61 | // static var previews: some View { 62 | // SetEditorView(redisKeyModel: redisKeyModel) 63 | // } 64 | //} 65 | -------------------------------------------------------------------------------- /redis-pro/Views/RedisEditorView/StringEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringEditView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/4/7. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct StringEditorView: View { 13 | var store: StoreOf 14 | var keyObjectStore: StoreOf 15 | private let logger = Logger(label: "string-editor") 16 | 17 | init(store: StoreOf) { 18 | self.store = store.scope(state: \.stringValueState, action: ValueStore.Action.stringValueAction) 19 | self.keyObjectStore = store.scope(state: \.keyObjectState, action: ValueStore.Action.keyObjectAction) 20 | } 21 | 22 | var body: some View { 23 | WithViewStore(self.store, observe: { $0 }) {viewStore in 24 | VStack(alignment: .leading, spacing: 0) { 25 | VStack(alignment: .leading, spacing: MTheme.V_SPACING){ 26 | MTextEditor(text: viewStore.$text) 27 | } 28 | .background(Color.init(NSColor.textBackgroundColor)) 29 | 30 | // footer 31 | HStack(alignment: .center, spacing: MTheme.V_SPACING) { 32 | KeyObjectBar(store: keyObjectStore) 33 | 34 | if (viewStore.isIntactString) { 35 | FormText(label: "Length:", value: "\(viewStore.length)") 36 | } else { 37 | Text("Range: 0~\(viewStore.stringMaxLength + 1) / \(viewStore.length)") 38 | MButton(text: "Show Intact", action: {viewStore.send(.getIntactString)}) 39 | } 40 | 41 | Spacer() 42 | MButton(text: "Pretty Json", action: {viewStore.send(.jsonFormat)}) 43 | IconButton(icon: "arrow.clockwise", name: "Refresh", action: {viewStore.send(.refresh)}) 44 | IconButton(icon: "checkmark", name: "Submit", disabled: !viewStore.isIntactString, action: {viewStore.send(.submit)}) 45 | } 46 | .padding(EdgeInsets(top: MTheme.V_SPACING, leading: 0, bottom: 0, trailing: 0)) 47 | 48 | } 49 | 50 | .onAppear { 51 | logger.info("redis string value editor view appear ...") 52 | } 53 | } 54 | } 55 | 56 | } 57 | 58 | //struct StringEditView_Previews: PreviewProvider { 59 | // static var redisKeyModel:RedisKeyModel = RedisKeyModel(key: "tes", type: "string") 60 | // static var previews: some View { 61 | // StringEditorView(redisKeyModel: redisKeyModel) 62 | // } 63 | //} 64 | -------------------------------------------------------------------------------- /redis-pro/Views/RedisEditorView/ZSetEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZSetEditorView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/7. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct ZSetEditorView: View { 13 | 14 | var store:StoreOf 15 | var keyObjectStore: StoreOf 16 | let logger = Logger(label: "redis-set-editor") 17 | 18 | 19 | init(store: StoreOf) { 20 | self.store = store.scope(state: \.zsetValueState, action: ValueStore.Action.zsetValueAction) 21 | self.keyObjectStore = store.scope(state: \.keyObjectState, action: ValueStore.Action.keyObjectAction) 22 | } 23 | 24 | var body: some View { 25 | 26 | WithViewStore(self.store, observe: { $0 }) { viewStore in 27 | VStack(alignment: .leading, spacing: 0) { 28 | HStack(alignment: .center , spacing: 4) { 29 | IconButton(icon: "plus", name: "Add", action: {viewStore.send(.addNew)}) 30 | IconButton(icon: "trash", name: "Delete", disabled: viewStore.tableState.selectIndex < 0, action: {viewStore.send(.deleteConfirm(viewStore.tableState.selectIndex))}) 31 | 32 | SearchBar(placeholder: "Search element...", onCommit: {viewStore.send(.search($0))}) 33 | PageBar(store: store.scope(state: \.pageState, action: ZSetValueStore.Action.pageAction)) 34 | } 35 | .padding(EdgeInsets(top: MTheme.V_SPACING, leading: 0, bottom: MTheme.V_SPACING, trailing: 0)) 36 | 37 | NTableView(store: store.scope(state: \.tableState, action: ZSetValueStore.Action.tableAction)) 38 | 39 | // footer 40 | HStack(alignment: .center, spacing: 4) { 41 | KeyObjectBar(store: keyObjectStore) 42 | Spacer() 43 | IconButton(icon: "arrow.clockwise", name: "Refresh", action: {viewStore.send(.refresh)}) 44 | } 45 | .padding(EdgeInsets(top: MTheme.V_SPACING, leading: 0, bottom: 0, trailing: 0)) 46 | } 47 | .sheet(isPresented: viewStore.$editModalVisible, onDismiss: { 48 | }) { 49 | ModalView("Edit zset element", action: {viewStore.send(.submit)}) { 50 | VStack(alignment:.leading, spacing: MTheme.H_SPACING) { 51 | FormItemDouble(label: "Score", placeholder: "score", value: viewStore.$editScore) 52 | FormItemTextArea(label: "Value", placeholder: "value", value: viewStore.$editValue) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | } 60 | 61 | //struct ZSetEditorView_Previews: PreviewProvider { 62 | // static var redisKeyModel:RedisKeyModel = RedisKeyModel(key: "tes", type: "string") 63 | // static var previews: some View { 64 | // ZSetEditorView(redisKeyModel: redisKeyModel) 65 | // } 66 | //} 67 | -------------------------------------------------------------------------------- /redis-pro/Views/Sidebar/DatabasePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabasePicker.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/5/10. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | 11 | struct DatabasePicker: View { 12 | 13 | var store:StoreOf 14 | 15 | var body: some View { 16 | WithViewStore(self.store, observe: { $0 }) {viewStore in 17 | 18 | Menu(content: { 19 | ForEach(0 ..< viewStore.databases, id: \.self) { item in 20 | Button("DB\(item)", action: { viewStore.send(.selectDB(item))}) 21 | .font(.system(size: 10.0)) 22 | .foregroundColor(.primary) 23 | } 24 | }, label: { 25 | MLabel(name: "DB\(viewStore.database)", icon: "cylinder.split.1x2").font(.system(size: 8)) 26 | }) 27 | .scaleEffect(0.9) 28 | .frame(width:56) 29 | .menuStyle(BorderlessButtonMenuStyle()) 30 | .onAppear{ 31 | viewStore.send(.initial) 32 | } 33 | } 34 | } 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /redis-pro/Views/System/ClientsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientsListView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/18. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct ClientsListView: View { 13 | 14 | var store:StoreOf 15 | 16 | var body: some View { 17 | WithViewStore(self.store, observe: { $0 }) {viewStore in 18 | VStack(alignment: .leading, spacing: MTheme.V_SPACING) { 19 | 20 | NTableView(store: store.scope(state: \.tableState, action: ClientListStore.Action.tableAction)) 21 | 22 | HStack(alignment: .center , spacing: 8) { 23 | Spacer() 24 | MButton(text: "Kill Client", action: {viewStore.send(.killConfirm(viewStore.tableState.selectIndex))}, disabled: viewStore.tableState.selectIndex < 0) 25 | MButton(text: "Refresh", action: {viewStore.send(.refresh)}) 26 | } 27 | } 28 | .onAppear { 29 | viewStore.send(.initial) 30 | } 31 | } 32 | } 33 | 34 | } 35 | 36 | //struct ClientsListView_Previews: PreviewProvider { 37 | // static var previews: some View { 38 | // ClientsListView() 39 | // } 40 | //} 41 | -------------------------------------------------------------------------------- /redis-pro/Views/System/LuaView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LuaView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/7/17. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | 13 | struct LuaView: View { 14 | var store:StoreOf 15 | let logger = Logger(label: "lua-view") 16 | 17 | var body: some View { 18 | WithViewStore(self.store, observe: { $0 }) {viewStore in 19 | VStack(alignment: .leading, spacing: MTheme.V_SPACING) { 20 | 21 | // header 22 | HStack(alignment: .center, spacing: MTheme.H_SPACING) { 23 | Text("Eval Lua Script") 24 | Spacer() 25 | MButton(text: "Script Flush", action: { viewStore.send(.scriptFlush) }) 26 | } 27 | 28 | VSplitView { 29 | VStack(alignment: .leading, spacing: MTheme.V_SPACING){ 30 | // text editor 31 | MTextEditor(text: viewStore.$lua) 32 | 33 | // btns 34 | HStack(alignment: .center, spacing: MTheme.H_SPACING) { 35 | // Text("Script SHA: \(viewStore.luaSHA)") 36 | Spacer() 37 | // MButton(text: "Script Kill", action: { viewStore.send(.scriptKill) }) 38 | // MButton(text: "Eval SHA", action: { viewStore.send(.eval) }) 39 | MButton(text: "Eval", action: { viewStore.send(.eval) }, keyEquivalent: .return) 40 | } 41 | .padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)) 42 | 43 | } 44 | 45 | MTextEditor(text: viewStore.$evalResult) 46 | } 47 | 48 | } 49 | } 50 | } 51 | } 52 | 53 | //struct LuaView_Previews: PreviewProvider { 54 | // static var previews: some View { 55 | // LuaView() 56 | // } 57 | //} 58 | -------------------------------------------------------------------------------- /redis-pro/Views/System/RedisConfigView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisConfigView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/21. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct RedisConfigView: View { 13 | 14 | var store:StoreOf 15 | let logger = Logger(label: "redis-config-view") 16 | 17 | var body: some View { 18 | 19 | WithViewStore(self.store, observe: { $0 }) {viewStore in 20 | VStack(alignment: .leading, spacing: MTheme.V_SPACING) { 21 | HStack(alignment: .center , spacing: MTheme.H_SPACING) { 22 | 23 | SearchBar(placeholder: "Search config...", onCommit: {viewStore.send(.search($0))}) 24 | 25 | Spacer() 26 | MButton(text: "Rewrite", action: {viewStore.send(.rewrite)}) 27 | .help("REDIS_CONFIG_REWRITE") 28 | }.padding(MTheme.HEADER_PADDING) 29 | 30 | NTableView(store: store.scope(state: \.tableState, action: RedisConfigStore.Action.tableAction)) 31 | 32 | HStack(alignment: .center , spacing: MTheme.H_SPACING) { 33 | Spacer() 34 | MButton(text: "Refresh", action: {viewStore.send(.refresh)}) 35 | } 36 | } 37 | .sheet(isPresented: viewStore.$editModalVisible, onDismiss: { 38 | }) { 39 | ModalView("Edit Config Key: \(viewStore.editKey)", action: {viewStore.send(.submit)}) { 40 | VStack(alignment:.leading, spacing: MTheme.V_SPACING) { 41 | MTextView(text: viewStore.$editValue) 42 | } 43 | .frame(minWidth:500, minHeight:300) 44 | } 45 | } 46 | .onAppear { 47 | viewStore.send(.initial) 48 | } 49 | } 50 | } 51 | 52 | } 53 | 54 | //struct RedisConfigView_Previews: PreviewProvider { 55 | // static var previews: some View { 56 | // RedisConfigView() 57 | // } 58 | //} 59 | -------------------------------------------------------------------------------- /redis-pro/Views/System/RedisInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisInfoView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/6/10. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct RedisInfoView: View { 13 | var store:StoreOf 14 | 15 | var body: some View { 16 | WithViewStore(self.store, observe: { $0 }) {viewStore in 17 | VStack(alignment: .leading, spacing: MTheme.V_SPACING) { 18 | TabView(selection: viewStore.binding(get: \.section, send: RedisInfoStore.Action.setTab)) { 19 | ForEach(viewStore.redisInfoModels.indices, id:\.self) { index in 20 | NTableView(store: store.scope(state: \.tableState, action: RedisInfoStore.Action.tableAction)) 21 | .tabItem { 22 | Text(viewStore.redisInfoModels[index].section) 23 | } 24 | .tag(viewStore.redisInfoModels[index].section) 25 | } 26 | } 27 | .frame(minWidth: 500, minHeight: 600) 28 | 29 | HStack(alignment: .center , spacing: MTheme.H_SPACING) { 30 | Spacer() 31 | MButton(text: "Reset State", action: {viewStore.send(.resetState)}) 32 | MButton(text: "Refresh", action: {viewStore.send(.refresh)}) 33 | } 34 | } 35 | .onAppear { 36 | viewStore.send(.initial) 37 | } 38 | } 39 | } 40 | } 41 | 42 | //struct RedisInfoView_Previews: PreviewProvider { 43 | // 44 | // static var redisInstanceModel:RedisInstanceModel = RedisInstanceModel(redisModel: RedisModel(password: "")) 45 | // 46 | // static var previews: some View { 47 | // RedisInfoView().environmentObject(redisInstanceModel) 48 | // } 49 | // 50 | //} 51 | -------------------------------------------------------------------------------- /redis-pro/Views/System/RedisSystemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisSystemView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpan on 2022/6/4. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct RedisSystemView: View { 13 | var store:StoreOf 14 | 15 | var body: some View { 16 | WithViewStore(self.store, observe: { $0.systemView }) {viewStore in 17 | 18 | if viewStore.state == RedisSystemViewTypeEnum.REDIS_INFO { 19 | RedisInfoView(store: store.scope(state: \.redisInfoState, action: RedisSystemStore.Action.redisInfoAction)) 20 | } else if viewStore.state == RedisSystemViewTypeEnum.REDIS_INFO { 21 | RedisInfoView(store: store.scope(state: \.redisInfoState, action: RedisSystemStore.Action.redisInfoAction)) 22 | } else if viewStore.state == RedisSystemViewTypeEnum.CLIENT_LIST { 23 | ClientsListView(store: store.scope(state: \.clientListState, action: RedisSystemStore.Action.clientListAction)) 24 | } else if viewStore.state == RedisSystemViewTypeEnum.SLOW_LOG { 25 | SlowLogView(store: store.scope(state: \.slowLogState, action: RedisSystemStore.Action.slowLogAction)) 26 | } else if viewStore.state == RedisSystemViewTypeEnum.REDIS_CONFIG { 27 | RedisConfigView(store: store.scope(state: \.redisConfigState, action: RedisSystemStore.Action.redisConfigAction)) 28 | } else if viewStore.state == RedisSystemViewTypeEnum.LUA { 29 | LuaView(store: store.scope(state: \.luaState, action: RedisSystemStore.Action.luaAction)) 30 | } else { 31 | EmptyView() 32 | } 33 | } 34 | } 35 | } 36 | 37 | //struct RedisSystemView_Previews: PreviewProvider { 38 | // static var previews: some View { 39 | // RedisSystemView() 40 | // } 41 | //} 42 | -------------------------------------------------------------------------------- /redis-pro/Views/System/SlowLogView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlowLogView.swift 3 | // redis-pro 4 | // 5 | // Created by chengpanwang on 2021/7/14. 6 | // 7 | 8 | import SwiftUI 9 | import Logging 10 | import ComposableArchitecture 11 | 12 | struct SlowLogView: View { 13 | var store:StoreOf 14 | let logger = Logger(label: "slow-log-view") 15 | 16 | var body: some View { 17 | WithViewStore(self.store, observe: { $0 }) {viewStore in 18 | VStack(alignment: .leading, spacing: MTheme.V_SPACING) { 19 | // header 20 | HStack(alignment: .center, spacing: MTheme.H_SPACING) { 21 | FormItemInt(label: "Slower Than(us)", labelWidth: 120, value: viewStore.$slowerThan, suffix: "square.and.pencil", onCommit: {viewStore.send(.setSlowerThan)}) 22 | .help("REDIS_SLOW_LOG_SLOWER_THAN") 23 | .frame(width: 320) 24 | FormItemInt(label: "Max Len", value: viewStore.$maxLen, suffix: "square.and.pencil", onCommit: {viewStore.send(.setMaxLen)}) 25 | .help("REDIS_SLOW_LOG_MAX_LEN") 26 | .frame(width: 200) 27 | 28 | FormItemInt(label: "Size", value: viewStore.$size, suffix: "square.and.pencil", onCommit: {viewStore.send(.setSize)}) 29 | .help("REDIS_SLOW_LOG_SIZE") 30 | .frame(width: 200) 31 | 32 | Spacer() 33 | MButton(text: "Reset", action: {viewStore.send(.reset)}) 34 | .help("REDIS_SLOW_LOG_RESET") 35 | } 36 | 37 | NTableView(store: store.scope(state: \.tableState, action: SlowLogStore.Action.tableAction)) 38 | 39 | // footer 40 | HStack(alignment: .center, spacing: MTheme.H_SPACING_L) { 41 | Spacer() 42 | Text("Total: \(viewStore.total)") 43 | .font(.system(size: 12)) 44 | .help("REDIS_SLOW_LOG_TOTAL") 45 | Text("Current: \(viewStore.tableState.datasource.count)") 46 | .font(.system(size: 12)) 47 | .help("REDIS_SLOW_LOG_SIZE") 48 | IconButton(icon: "arrow.clockwise", name: "Refresh", action: {viewStore.send(.refresh)}) 49 | } 50 | .padding(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) 51 | }.onAppear { 52 | viewStore.send(.initial) 53 | } 54 | } 55 | } 56 | } 57 | 58 | //struct SlowLogView_Previews: PreviewProvider { 59 | // static var redisInstanceModel:RedisInstanceModel = RedisInstanceModel(redisModel: RedisModel()) 60 | // 61 | // static var previews: some View { 62 | // SlowLogView() 63 | // .environmentObject(redisInstanceModel) 64 | // } 65 | //} 66 | -------------------------------------------------------------------------------- /redis-pro/redis-proRelease.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.downloads.read-only 8 | 9 | com.apple.security.files.user-selected.read-write 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.network.server 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /redis-pro/redis_pro.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.downloads.read-only 8 | 9 | com.apple.security.files.user-selected.read-write 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.network.server 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------