release(ios): 0.2.2 移除分组改用标签归类并完成石墨配色重构
- 删除分组实体:分组视图/编辑器/仓储与 groupId 全部移除,升级首启清理 instance-groups.json 与 fleet.groupMode - 新增批量标签表:三态覆盖度(未贴 / N/M / 全部),整批加减与新建标签 - 配色重构,全应用零蓝色:石墨墨色承担主操作与选中态,颜色只留给状态语义; 浅色改中性冷灰,深色改低明度石墨三层 - 选中态改实心主色 + onPrimary,替换不可读的低透明度色雾(主题块、图例胶囊) - 应用图标重做为扁平石墨底 + 黄铜 SIM 面板,参数化光栅化脚本与参考稿 - 运行概览:检测进度并入副标题,读数栅格固定八格两行并保留不可用占位 - 实例详情:仪表改两列发丝线栅格,缺数据留轨道,修掉头部高度回弹; 补齐设备名称/存储总量/可用存储/使用率四个字段名 - 版本号显示去掉括号,设置页只显示 0.2.2 - 文档:新增 docs/ios-native-0.2.2.md,同步 ios-native.md 与两处 README
This commit is contained in:
@@ -37,7 +37,7 @@ final class FleetViewModelTests: XCTestCase {
|
||||
XCTAssertTrue(model.rows.allSatisfy { $0.latencyMilliseconds != nil })
|
||||
}
|
||||
|
||||
func testVisibleRowsAndGroupModesUseFilteredResults() async throws {
|
||||
func testVisibleRowsFollowSearchAndTagFilters() async throws {
|
||||
let repository = MemoryInstanceRepository()
|
||||
let model = FleetViewModel(
|
||||
repository: repository,
|
||||
@@ -52,7 +52,6 @@ final class FleetViewModelTests: XCTestCase {
|
||||
model.searchText = "home"
|
||||
|
||||
XCTAssertEqual(model.visibleRows.map(\.instance.name), ["Home"])
|
||||
XCTAssertEqual(model.groupedRows.flatMap(\.rows).map(\.instance.name), ["Home"])
|
||||
|
||||
model.searchText = ""
|
||||
model.clearTagFilters()
|
||||
@@ -61,9 +60,22 @@ final class FleetViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(model.visibleRows.map(\.instance.name), ["Home"])
|
||||
XCTAssertEqual(model.countForTagFilter("home"), 1)
|
||||
model.clearTagFilters()
|
||||
XCTAssertEqual(model.visibleRows.count, 2, "没有筛选时全部节点都要出现")
|
||||
}
|
||||
|
||||
model.groupMode = .status
|
||||
XCTAssertEqual(model.groupedRows.map(\.title), ["在线"])
|
||||
func testVisibleRowsPutProblemNodesFirst() async throws {
|
||||
let model = FleetViewModel(
|
||||
repository: MemoryInstanceRepository(),
|
||||
probe: ConnectionProbe(client: UnreachableForClient(host: "192.168.1.99")),
|
||||
credentials: NoopCredentialStore(),
|
||||
preferences: transientPreferences()
|
||||
)
|
||||
try await model.add(InstanceInput(name: "Healthy", origin: URL(string: "http://192.168.1.98:3000")!))
|
||||
try await model.add(InstanceInput(name: "Broken", origin: URL(string: "http://192.168.1.99:3000")!))
|
||||
|
||||
await model.refresh()
|
||||
|
||||
XCTAssertEqual(model.visibleRows.map(\.instance.name), ["Broken", "Healthy"])
|
||||
}
|
||||
|
||||
func testTagFiltersCombineWithAndSemanticsAndPruneStaleSelections() async throws {
|
||||
@@ -129,89 +141,58 @@ final class FleetViewModelTests: XCTestCase {
|
||||
UserDefaults(suiteName: suite)?.removePersistentDomain(forName: suite)
|
||||
}
|
||||
|
||||
func testGroupModeKeepsUngroupedInstancesVisible() async throws {
|
||||
let repository = MemoryInstanceRepository()
|
||||
func testUpdateRewritesNameAndTags() async throws {
|
||||
let model = FleetViewModel(
|
||||
repository: repository,
|
||||
probe: ConnectionProbe(client: AlwaysOnlineClient()),
|
||||
credentials: NoopCredentialStore()
|
||||
)
|
||||
try await model.add(InstanceInput(name: "Ungrouped", origin: URL(string: "http://192.168.1.72:3000")!))
|
||||
|
||||
await model.refresh()
|
||||
model.groupMode = .groups
|
||||
|
||||
XCTAssertEqual(model.groupedRows.map(\.title), ["未分组"])
|
||||
XCTAssertEqual(model.groupedRows.first?.rows.map(\.instance.name), ["Ungrouped"])
|
||||
}
|
||||
|
||||
func testStoredGroupsResolveNamesAndCanUpdateInstance() async throws {
|
||||
let location = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathComponent("groups.json")
|
||||
let groupStore = FileInstanceGroupStore(location: location)
|
||||
let repository = MemoryInstanceRepository()
|
||||
let model = FleetViewModel(
|
||||
repository: repository,
|
||||
repository: MemoryInstanceRepository(),
|
||||
probe: ConnectionProbe(client: AlwaysOnlineClient()),
|
||||
credentials: NoopCredentialStore(),
|
||||
groupStore: groupStore
|
||||
preferences: transientPreferences()
|
||||
)
|
||||
|
||||
let createdGroup = try await groupStore.create(InstanceGroupInput(name: "外场"))
|
||||
try await model.add(InstanceInput(
|
||||
name: "Van",
|
||||
origin: URL(string: "http://192.168.1.80:3000")!,
|
||||
groupId: createdGroup.id
|
||||
tags: ["old"]
|
||||
))
|
||||
|
||||
await model.refresh()
|
||||
model.groupMode = .groups
|
||||
|
||||
XCTAssertEqual(model.groups.map(\.name), ["外场"])
|
||||
XCTAssertEqual(model.groupedRows.map(\.title), ["外场"])
|
||||
XCTAssertEqual(model.groupedRows.first?.rows.map(\.instance.name), ["Van"])
|
||||
|
||||
let row = try XCTUnwrap(model.rows.first)
|
||||
|
||||
try await model.update(row.id, InstanceInput(
|
||||
name: "Van Pro",
|
||||
origin: row.instance.origin,
|
||||
tags: ["field"],
|
||||
groupId: createdGroup.id
|
||||
tags: ["field"]
|
||||
))
|
||||
|
||||
XCTAssertEqual(model.rows.first?.instance.name, "Van Pro")
|
||||
XCTAssertEqual(model.rows.first?.instance.tags, ["field"])
|
||||
}
|
||||
|
||||
func testAssignMovesSelectedInstancesIntoAndOutOfGroup() async throws {
|
||||
let location = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathComponent("groups.json")
|
||||
let groupStore = FileInstanceGroupStore(location: location)
|
||||
let repository = MemoryInstanceRepository()
|
||||
func testApplyTagAddsAndRemovesAcrossTheWholeSelection() async throws {
|
||||
let model = FleetViewModel(
|
||||
repository: repository,
|
||||
repository: MemoryInstanceRepository(),
|
||||
probe: ConnectionProbe(client: AlwaysOnlineClient()),
|
||||
credentials: NoopCredentialStore(),
|
||||
groupStore: groupStore
|
||||
preferences: transientPreferences()
|
||||
)
|
||||
let first = try await model.add(InstanceInput(
|
||||
name: "First",
|
||||
origin: URL(string: "http://192.168.1.90:3000")!
|
||||
origin: URL(string: "http://192.168.1.90:3000")!,
|
||||
tags: ["keep"]
|
||||
))
|
||||
let second = try await model.add(InstanceInput(
|
||||
name: "Second",
|
||||
origin: URL(string: "http://192.168.1.91:3000")!
|
||||
))
|
||||
let group = try await model.createGroup(named: "外场")
|
||||
await model.refresh()
|
||||
let ids = [first.id, second.id]
|
||||
|
||||
try await model.assign(groupID: group.id, to: [first.id, second.id])
|
||||
XCTAssertEqual(model.rows.map(\.instance.groupId), [group.id, group.id])
|
||||
await model.applyTag("外场", to: ids)
|
||||
XCTAssertEqual(model.rows.map(\.instance.tags).map { $0.contains("外场") }, [true, true])
|
||||
XCTAssertEqual(model.tagCoverage("外场", in: ids), 2)
|
||||
|
||||
try await model.assign(groupID: nil, to: [first.id])
|
||||
XCTAssertEqual(model.rows.first?.instance.groupId, nil)
|
||||
XCTAssertEqual(model.rows.last?.instance.groupId, group.id)
|
||||
// 再点一次只撕掉这一个标签,其他标签必须留下。
|
||||
await model.applyTag("外场", to: [first.id], removing: true)
|
||||
XCTAssertEqual(model.rows.first?.instance.tags, ["keep"])
|
||||
XCTAssertEqual(model.tagCoverage("外场", in: ids), 1)
|
||||
}
|
||||
|
||||
func testAddValidatesPasswordAndDeleteRemovesCredential() async throws {
|
||||
@@ -274,6 +255,18 @@ struct AlwaysOnlineClient: SimAdminClientProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
/// 指定主机一律不可达,用来验证"问题节点排在前面"。
|
||||
struct UnreachableForClient: SimAdminClientProtocol {
|
||||
let host: String
|
||||
|
||||
func send(_: UpstreamRoute, baseURL: URL, cookie: String?) async throws -> UpstreamResponse {
|
||||
if baseURL.host == host {
|
||||
throw UpstreamError.transportFailed
|
||||
}
|
||||
return UpstreamResponse(status: 200, data: Data("{}".utf8))
|
||||
}
|
||||
}
|
||||
|
||||
final class NoopCredentialStore: @unchecked Sendable, CredentialStore {
|
||||
private let lock = NSLock()
|
||||
private var savedStorage: [UUID: String] = [:]
|
||||
@@ -327,7 +320,6 @@ final class MemoryInstanceRepository: @unchecked Sendable, InstanceRepository {
|
||||
name: input.name,
|
||||
origin: input.origin,
|
||||
tags: input.tags,
|
||||
groupId: input.groupId,
|
||||
credentialConfigured: !(input.password ?? "").isEmpty,
|
||||
configRevision: 1,
|
||||
createdAt: now,
|
||||
@@ -348,7 +340,6 @@ final class MemoryInstanceRepository: @unchecked Sendable, InstanceRepository {
|
||||
if let name = patch.name { instance.name = name }
|
||||
if let origin = patch.origin { instance.origin = origin }
|
||||
if let tags = patch.tags { instance.tags = tags }
|
||||
if patch.groupId != nil || instance.groupId != nil { instance.groupId = patch.groupId }
|
||||
if let password = patch.password {
|
||||
instance.credentialConfigured = !password.isEmpty
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user