mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat: 增强自动加分规则管理,支持规则的创建、更新、删除和持久化
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Emitter, State};
|
use tauri::{AppHandle, Emitter, State};
|
||||||
|
|
||||||
use crate::services::{
|
use crate::services::{
|
||||||
AutoScoreAction, AutoScoreRule, AutoScoreTrigger, PermissionLevel, SettingsKey, SettingsValue,
|
AutoScoreAction, AutoScoreRule, AutoScoreService, AutoScoreTrigger, PermissionLevel,
|
||||||
|
SettingsKey, SettingsValue,
|
||||||
};
|
};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
@@ -51,6 +53,101 @@ pub struct AutoScoreStatus {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_rule_from_create(rule: CreateAutoScoreRule) -> AutoScoreRule {
|
||||||
|
AutoScoreRule {
|
||||||
|
id: 0,
|
||||||
|
name: rule.name,
|
||||||
|
enabled: rule.enabled,
|
||||||
|
student_names: rule.student_names,
|
||||||
|
triggers: rule.triggers,
|
||||||
|
actions: rule.actions,
|
||||||
|
last_executed: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_rule_from_update(rule: UpdateAutoScoreRule) -> AutoScoreRule {
|
||||||
|
AutoScoreRule {
|
||||||
|
id: rule.id,
|
||||||
|
name: rule.name,
|
||||||
|
enabled: rule.enabled,
|
||||||
|
student_names: rule.student_names,
|
||||||
|
triggers: rule.triggers,
|
||||||
|
actions: rule.actions,
|
||||||
|
last_executed: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_rules_from_settings(
|
||||||
|
state: &Arc<RwLock<AppState>>,
|
||||||
|
) -> Result<Vec<AutoScoreRule>, String> {
|
||||||
|
let state_guard = state.read();
|
||||||
|
let db_conn = state_guard.db.read().clone();
|
||||||
|
let mut settings = state_guard.settings.write();
|
||||||
|
settings.attach_db(db_conn);
|
||||||
|
settings.initialize().await?;
|
||||||
|
|
||||||
|
let rules_json = match settings.get_value(SettingsKey::AutoScoreRules) {
|
||||||
|
SettingsValue::Json(value) => value,
|
||||||
|
_ => JsonValue::Array(vec![]),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AutoScoreService::deserialize_rules(&rules_json))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn persist_rules_to_settings(
|
||||||
|
state: &Arc<RwLock<AppState>>,
|
||||||
|
rules: &[AutoScoreRule],
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let rules_json = AutoScoreService::serialize_rules(rules)?;
|
||||||
|
let enabled = rules.iter().any(|rule| rule.enabled);
|
||||||
|
|
||||||
|
let state_guard = state.read();
|
||||||
|
let db_conn = state_guard.db.read().clone();
|
||||||
|
let mut settings = state_guard.settings.write();
|
||||||
|
settings.attach_db(db_conn);
|
||||||
|
settings.initialize().await?;
|
||||||
|
settings
|
||||||
|
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
|
||||||
|
.await?;
|
||||||
|
settings
|
||||||
|
.set_value(
|
||||||
|
SettingsKey::AutoScoreEnabled,
|
||||||
|
SettingsValue::Boolean(enabled),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_cached_rules(state: &Arc<RwLock<AppState>>, rules: Vec<AutoScoreRule>) {
|
||||||
|
let state_guard = state.read();
|
||||||
|
let mut auto_score_service = state_guard.auto_score.write();
|
||||||
|
auto_score_service.replace_rules(rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_cached_rules(state: &Arc<RwLock<AppState>>) -> Result<Vec<AutoScoreRule>, String> {
|
||||||
|
let rules = load_rules_from_settings(state).await?;
|
||||||
|
replace_cached_rules(state, rules.clone());
|
||||||
|
Ok(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_rules_changed(app_handle: &AppHandle, state: &Arc<RwLock<AppState>>) {
|
||||||
|
let state_guard = state.read();
|
||||||
|
let rules = state_guard.auto_score.read().get_rules().to_vec();
|
||||||
|
let _ = app_handle.emit("auto-score:rulesChanged", &rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_auto_score_status_changed(app_handle: &AppHandle, rules: &[AutoScoreRule]) {
|
||||||
|
let enabled = rules.iter().any(|rule| rule.enabled);
|
||||||
|
let _ = app_handle.emit(
|
||||||
|
"settings:changed",
|
||||||
|
serde_json::json!({
|
||||||
|
"key": "auto_score_enabled",
|
||||||
|
"value": enabled,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn auto_score_get_rules(
|
pub async fn auto_score_get_rules(
|
||||||
sender_id: Option<u32>,
|
sender_id: Option<u32>,
|
||||||
@@ -64,9 +161,7 @@ pub async fn auto_score_get_rules(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let state_guard = state.read();
|
let rules = sync_cached_rules(state.inner()).await?;
|
||||||
let auto_score_service = state_guard.auto_score.read();
|
|
||||||
let rules = auto_score_service.get_rules().to_vec();
|
|
||||||
Ok(IpcResponse::success(rules))
|
Ok(IpcResponse::success(rules))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,39 +180,19 @@ pub async fn auto_score_add_rule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_id = {
|
let current_rules = sync_cached_rules(state.inner()).await?;
|
||||||
let state_guard = state.read();
|
let mut working_service = AutoScoreService::from_rules(current_rules);
|
||||||
let db_conn = state_guard.db.read().clone();
|
let new_id = match working_service.add_rule(build_rule_from_create(rule)) {
|
||||||
let mut auto_score_service = state_guard.auto_score.write();
|
Ok(value) => value,
|
||||||
let mut settings = state_guard.settings.write();
|
Err(message) => return Ok(IpcResponse::error(&message)),
|
||||||
settings.attach_db(db_conn);
|
|
||||||
settings.initialize().await?;
|
|
||||||
|
|
||||||
let new_rule = AutoScoreRule {
|
|
||||||
id: 0,
|
|
||||||
name: rule.name,
|
|
||||||
enabled: rule.enabled,
|
|
||||||
student_names: rule.student_names,
|
|
||||||
triggers: rule.triggers,
|
|
||||||
actions: rule.actions,
|
|
||||||
last_executed: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_id = auto_score_service.add_rule(new_rule);
|
|
||||||
|
|
||||||
let rules_json = auto_score_service.get_rules_json();
|
|
||||||
let _ = settings
|
|
||||||
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
new_id
|
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
let next_rules = working_service.into_rules();
|
||||||
let state_guard = state.read();
|
persist_rules_to_settings(state.inner(), &next_rules).await?;
|
||||||
let auto_score_service = state_guard.auto_score.read();
|
replace_cached_rules(state.inner(), next_rules);
|
||||||
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
|
let rules = state.read().auto_score.read().get_rules().to_vec();
|
||||||
}
|
emit_auto_score_status_changed(&app_handle, &rules);
|
||||||
|
emit_rules_changed(&app_handle, state.inner());
|
||||||
|
|
||||||
Ok(IpcResponse::success(new_id))
|
Ok(IpcResponse::success(new_id))
|
||||||
}
|
}
|
||||||
@@ -137,47 +212,20 @@ pub async fn auto_score_update_rule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let success = {
|
let current_rules = sync_cached_rules(state.inner()).await?;
|
||||||
let state_guard = state.read();
|
let mut working_service = AutoScoreService::from_rules(current_rules);
|
||||||
let db_conn = state_guard.db.read().clone();
|
if let Err(message) = working_service.update_rule(build_rule_from_update(rule)) {
|
||||||
let mut auto_score_service = state_guard.auto_score.write();
|
return Ok(IpcResponse::error(&message));
|
||||||
let mut settings = state_guard.settings.write();
|
|
||||||
settings.attach_db(db_conn);
|
|
||||||
settings.initialize().await?;
|
|
||||||
|
|
||||||
let existing = auto_score_service.get_rule_by_id(rule.id);
|
|
||||||
let last_executed = existing.and_then(|r| r.last_executed.clone());
|
|
||||||
|
|
||||||
let updated_rule = AutoScoreRule {
|
|
||||||
id: rule.id,
|
|
||||||
name: rule.name,
|
|
||||||
enabled: rule.enabled,
|
|
||||||
student_names: rule.student_names,
|
|
||||||
triggers: rule.triggers,
|
|
||||||
actions: rule.actions,
|
|
||||||
last_executed,
|
|
||||||
};
|
|
||||||
|
|
||||||
let success = auto_score_service.update_rule(updated_rule);
|
|
||||||
|
|
||||||
if success {
|
|
||||||
let rules_json = auto_score_service.get_rules_json();
|
|
||||||
let _ = settings
|
|
||||||
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
success
|
|
||||||
};
|
|
||||||
|
|
||||||
if success {
|
|
||||||
let state_guard = state.read();
|
|
||||||
let auto_score_service = state_guard.auto_score.read();
|
|
||||||
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
|
|
||||||
Ok(IpcResponse::success(true))
|
|
||||||
} else {
|
|
||||||
Ok(IpcResponse::error("Rule not found"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let next_rules = working_service.into_rules();
|
||||||
|
persist_rules_to_settings(state.inner(), &next_rules).await?;
|
||||||
|
replace_cached_rules(state.inner(), next_rules);
|
||||||
|
let rules = state.read().auto_score.read().get_rules().to_vec();
|
||||||
|
emit_auto_score_status_changed(&app_handle, &rules);
|
||||||
|
emit_rules_changed(&app_handle, state.inner());
|
||||||
|
|
||||||
|
Ok(IpcResponse::success(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -195,34 +243,20 @@ pub async fn auto_score_delete_rule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let success = {
|
let current_rules = sync_cached_rules(state.inner()).await?;
|
||||||
let state_guard = state.read();
|
let mut working_service = AutoScoreService::from_rules(current_rules);
|
||||||
let db_conn = state_guard.db.read().clone();
|
if let Err(message) = working_service.delete_rule(rule_id) {
|
||||||
let mut auto_score_service = state_guard.auto_score.write();
|
return Ok(IpcResponse::error(&message));
|
||||||
let mut settings = state_guard.settings.write();
|
|
||||||
settings.attach_db(db_conn);
|
|
||||||
settings.initialize().await?;
|
|
||||||
|
|
||||||
let success = auto_score_service.delete_rule(rule_id);
|
|
||||||
|
|
||||||
if success {
|
|
||||||
let rules_json = auto_score_service.get_rules_json();
|
|
||||||
let _ = settings
|
|
||||||
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
success
|
|
||||||
};
|
|
||||||
|
|
||||||
if success {
|
|
||||||
let state_guard = state.read();
|
|
||||||
let auto_score_service = state_guard.auto_score.read();
|
|
||||||
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
|
|
||||||
Ok(IpcResponse::success(true))
|
|
||||||
} else {
|
|
||||||
Ok(IpcResponse::error("Rule not found"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let next_rules = working_service.into_rules();
|
||||||
|
persist_rules_to_settings(state.inner(), &next_rules).await?;
|
||||||
|
replace_cached_rules(state.inner(), next_rules);
|
||||||
|
let rules = state.read().auto_score.read().get_rules().to_vec();
|
||||||
|
emit_auto_score_status_changed(&app_handle, &rules);
|
||||||
|
emit_rules_changed(&app_handle, state.inner());
|
||||||
|
|
||||||
|
Ok(IpcResponse::success(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -240,34 +274,20 @@ pub async fn auto_score_toggle_rule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let success = {
|
let current_rules = sync_cached_rules(state.inner()).await?;
|
||||||
let state_guard = state.read();
|
let mut working_service = AutoScoreService::from_rules(current_rules);
|
||||||
let db_conn = state_guard.db.read().clone();
|
if let Err(message) = working_service.toggle_rule(params.rule_id, params.enabled) {
|
||||||
let mut auto_score_service = state_guard.auto_score.write();
|
return Ok(IpcResponse::error(&message));
|
||||||
let mut settings = state_guard.settings.write();
|
|
||||||
settings.attach_db(db_conn);
|
|
||||||
settings.initialize().await?;
|
|
||||||
|
|
||||||
let success = auto_score_service.toggle_rule(params.rule_id, params.enabled);
|
|
||||||
|
|
||||||
if success {
|
|
||||||
let rules_json = auto_score_service.get_rules_json();
|
|
||||||
let _ = settings
|
|
||||||
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
success
|
|
||||||
};
|
|
||||||
|
|
||||||
if success {
|
|
||||||
let state_guard = state.read();
|
|
||||||
let auto_score_service = state_guard.auto_score.read();
|
|
||||||
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
|
|
||||||
Ok(IpcResponse::success(true))
|
|
||||||
} else {
|
|
||||||
Ok(IpcResponse::error("Rule not found"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let next_rules = working_service.into_rules();
|
||||||
|
persist_rules_to_settings(state.inner(), &next_rules).await?;
|
||||||
|
replace_cached_rules(state.inner(), next_rules);
|
||||||
|
let rules = state.read().auto_score.read().get_rules().to_vec();
|
||||||
|
emit_auto_score_status_changed(&app_handle, &rules);
|
||||||
|
emit_rules_changed(&app_handle, state.inner());
|
||||||
|
|
||||||
|
Ok(IpcResponse::success(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -283,9 +303,8 @@ pub async fn auto_score_get_status(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let state_guard = state.read();
|
let rules = sync_cached_rules(state.inner()).await?;
|
||||||
let auto_score_service = state_guard.auto_score.read();
|
let enabled = rules.iter().any(|rule| rule.enabled);
|
||||||
let enabled = auto_score_service.is_enabled();
|
|
||||||
Ok(IpcResponse::success(AutoScoreStatus { enabled }))
|
Ok(IpcResponse::success(AutoScoreStatus { enabled }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,27 +323,16 @@ pub async fn auto_score_sort_rules(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
let current_rules = sync_cached_rules(state.inner()).await?;
|
||||||
let state_guard = state.read();
|
let mut working_service = AutoScoreService::from_rules(current_rules);
|
||||||
let db_conn = state_guard.db.read().clone();
|
working_service.sort_rules(&rule_ids);
|
||||||
let mut auto_score_service = state_guard.auto_score.write();
|
|
||||||
let mut settings = state_guard.settings.write();
|
|
||||||
settings.attach_db(db_conn);
|
|
||||||
settings.initialize().await?;
|
|
||||||
|
|
||||||
auto_score_service.sort_rules(&rule_ids);
|
let next_rules = working_service.into_rules();
|
||||||
|
persist_rules_to_settings(state.inner(), &next_rules).await?;
|
||||||
let rules_json = auto_score_service.get_rules_json();
|
replace_cached_rules(state.inner(), next_rules);
|
||||||
let _ = settings
|
let rules = state.read().auto_score.read().get_rules().to_vec();
|
||||||
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
|
emit_auto_score_status_changed(&app_handle, &rules);
|
||||||
.await;
|
emit_rules_changed(&app_handle, state.inner());
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let state_guard = state.read();
|
|
||||||
let auto_score_service = state_guard.auto_score.read();
|
|
||||||
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(IpcResponse::success(true))
|
Ok(IpcResponse::success(true))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,13 @@ pub async fn settings_set(
|
|||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings_key == SettingsKey::AutoScoreRules {
|
||||||
|
let state_guard = state.read();
|
||||||
|
let mut auto_score = state_guard.auto_score.write();
|
||||||
|
auto_score.load_rules(value.clone());
|
||||||
|
let _ = app_handle.emit("auto-score:rulesChanged", auto_score.get_rules());
|
||||||
|
}
|
||||||
|
|
||||||
let change = SettingChange {
|
let change = SettingChange {
|
||||||
key: key.clone(),
|
key: key.clone(),
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
use chrono::{DateTime, Months, Utc};
|
use chrono::{DateTime, Months, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use serde_json::Value as JsonValue;
|
||||||
|
use std::collections::HashSet;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
const DEFAULT_INTERVAL_MINUTES: i64 = 30;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct AutoScoreTrigger {
|
pub struct AutoScoreTrigger {
|
||||||
pub event: String,
|
pub event: String,
|
||||||
pub value: Option<String>,
|
pub value: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct AutoScoreAction {
|
pub struct AutoScoreAction {
|
||||||
pub event: String,
|
pub event: String,
|
||||||
pub value: Option<String>,
|
pub value: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
enum IntervalUnit {
|
enum IntervalUnit {
|
||||||
Minute,
|
Minute,
|
||||||
@@ -23,13 +26,13 @@ enum IntervalUnit {
|
|||||||
Month,
|
Month,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
struct IntervalTriggerValue {
|
struct IntervalTriggerValue {
|
||||||
amount: i64,
|
amount: i64,
|
||||||
unit: IntervalUnit,
|
unit: IntervalUnit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct AutoScoreRule {
|
pub struct AutoScoreRule {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -75,6 +78,13 @@ impl AutoScoreService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_rules(rules: Vec<AutoScoreRule>) -> Self {
|
||||||
|
Self {
|
||||||
|
rules,
|
||||||
|
initialized: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn initialize(
|
pub async fn initialize(
|
||||||
&mut self,
|
&mut self,
|
||||||
_app_handle: &AppHandle,
|
_app_handle: &AppHandle,
|
||||||
@@ -86,96 +96,116 @@ impl AutoScoreService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_rules(&mut self, rules_json: serde_json::Value) {
|
pub fn deserialize_rules(rules_json: &JsonValue) -> Vec<AutoScoreRule> {
|
||||||
if let serde_json::Value::Array(arr) = rules_json {
|
let JsonValue::Array(items) = rules_json else {
|
||||||
self.rules = arr
|
return Vec::new();
|
||||||
.into_iter()
|
};
|
||||||
.filter_map(|v| serde_json::from_value(v).ok())
|
|
||||||
.collect();
|
items
|
||||||
} else {
|
.iter()
|
||||||
self.rules = Vec::new();
|
.filter_map(|item| serde_json::from_value::<AutoScoreRule>(item.clone()).ok())
|
||||||
}
|
.filter_map(|rule| normalize_rule(rule).ok())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize_rules(rules: &[AutoScoreRule]) -> Result<JsonValue, String> {
|
||||||
|
serde_json::to_value(rules)
|
||||||
|
.map_err(|error| format!("Failed to serialize auto score rules: {}", error))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_rules(&mut self, rules_json: JsonValue) {
|
||||||
|
self.rules = Self::deserialize_rules(&rules_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_rules(&mut self, rules: Vec<AutoScoreRule>) {
|
||||||
|
self.rules = rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_rules(self) -> Vec<AutoScoreRule> {
|
||||||
|
self.rules
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rules_json(&self) -> serde_json::Value {
|
pub fn get_rules_json(&self) -> serde_json::Value {
|
||||||
serde_json::to_value(&self.rules).unwrap_or(serde_json::Value::Array(vec![]))
|
Self::serialize_rules(&self.rules).unwrap_or(JsonValue::Array(vec![]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rules(&self) -> &[AutoScoreRule] {
|
pub fn get_rules(&self) -> &[AutoScoreRule] {
|
||||||
&self.rules
|
&self.rules
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rules_mut(&mut self) -> &mut Vec<AutoScoreRule> {
|
pub fn add_rule(&mut self, rule: AutoScoreRule) -> Result<i32, String> {
|
||||||
&mut self.rules
|
let mut normalized_rule = normalize_rule(rule)?;
|
||||||
|
let new_id = self.rules.iter().map(|item| item.id).max().unwrap_or(0) + 1;
|
||||||
|
normalized_rule.id = new_id;
|
||||||
|
normalized_rule.last_executed = None;
|
||||||
|
self.rules.push(normalized_rule);
|
||||||
|
Ok(new_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_rule(&mut self, mut rule: AutoScoreRule) -> i32 {
|
pub fn update_rule(&mut self, rule: AutoScoreRule) -> Result<(), String> {
|
||||||
let new_id = self.rules.iter().map(|r| r.id).max().unwrap_or(0) + 1;
|
let Some(index) = self.rules.iter().position(|item| item.id == rule.id) else {
|
||||||
rule.id = new_id;
|
return Err("Rule not found".to_string());
|
||||||
rule.last_executed = None;
|
};
|
||||||
self.rules.push(rule);
|
|
||||||
new_id
|
let last_executed = self.rules[index].last_executed.clone();
|
||||||
|
let mut normalized_rule = normalize_rule(rule)?;
|
||||||
|
normalized_rule.id = self.rules[index].id;
|
||||||
|
normalized_rule.last_executed = normalize_last_executed(last_executed);
|
||||||
|
self.rules[index] = normalized_rule;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_rule(&mut self, rule: AutoScoreRule) -> bool {
|
pub fn delete_rule(&mut self, rule_id: i32) -> Result<(), String> {
|
||||||
if let Some(existing) = self.rules.iter_mut().find(|r| r.id == rule.id) {
|
|
||||||
*existing = rule;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_rule(&mut self, rule_id: i32) -> bool {
|
|
||||||
let before_len = self.rules.len();
|
let before_len = self.rules.len();
|
||||||
self.rules.retain(|r| r.id != rule_id);
|
self.rules.retain(|rule| rule.id != rule_id);
|
||||||
self.rules.len() < before_len
|
if self.rules.len() == before_len {
|
||||||
}
|
return Err("Rule not found".to_string());
|
||||||
|
|
||||||
pub fn toggle_rule(&mut self, rule_id: i32, enabled: bool) -> bool {
|
|
||||||
if let Some(rule) = self.rules.iter_mut().find(|r| r.id == rule_id) {
|
|
||||||
rule.enabled = enabled;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sort_rules(&mut self, rule_ids: &[i32]) -> bool {
|
pub fn toggle_rule(&mut self, rule_id: i32, enabled: bool) -> Result<(), String> {
|
||||||
let rule_map: HashMap<i32, AutoScoreRule> =
|
let Some(rule) = self.rules.iter_mut().find(|item| item.id == rule_id) else {
|
||||||
self.rules.drain(..).map(|r| (r.id, r)).collect();
|
return Err("Rule not found".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
let mut sorted_rules: Vec<AutoScoreRule> = Vec::new();
|
rule.enabled = enabled;
|
||||||
for id in rule_ids {
|
Ok(())
|
||||||
if let Some(rule) = rule_map.get(id) {
|
}
|
||||||
sorted_rules.push(rule.clone());
|
|
||||||
|
pub fn sort_rules(&mut self, rule_ids: &[i32]) {
|
||||||
|
let mut ordered_rules = Vec::with_capacity(self.rules.len());
|
||||||
|
let mut consumed = HashSet::new();
|
||||||
|
|
||||||
|
for rule_id in rule_ids {
|
||||||
|
if !consumed.insert(*rule_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rule) = self.rules.iter().find(|item| item.id == *rule_id).cloned() {
|
||||||
|
ordered_rules.push(rule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (_, rule) in rule_map {
|
for rule in &self.rules {
|
||||||
if !rule_ids.contains(&rule.id) {
|
if consumed.insert(rule.id) {
|
||||||
sorted_rules.push(rule);
|
ordered_rules.push(rule.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.rules = sorted_rules;
|
self.rules = ordered_rules;
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_enabled(&self) -> bool {
|
pub fn is_enabled(&self) -> bool {
|
||||||
self.rules.iter().any(|r| r.enabled)
|
self.rules.iter().any(|rule| rule.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rule_by_id(&self, id: i32) -> Option<&AutoScoreRule> {
|
pub fn get_rule_by_id(&self, id: i32) -> Option<&AutoScoreRule> {
|
||||||
self.rules.iter().find(|r| r.id == id)
|
self.rules.iter().find(|rule| rule.id == id)
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_rule_by_id_mut(&mut self, id: i32) -> Option<&mut AutoScoreRule> {
|
|
||||||
self.rules.iter_mut().find(|r| r.id == id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_rule_executed(&mut self, rule_id: i32) {
|
pub fn mark_rule_executed(&mut self, rule_id: i32) {
|
||||||
if let Some(rule) = self.rules.iter_mut().find(|r| r.id == rule_id) {
|
if let Some(rule) = self.rules.iter_mut().find(|item| item.id == rule_id) {
|
||||||
rule.last_executed = Some(Utc::now().to_rfc3339());
|
rule.last_executed = Some(Utc::now().to_rfc3339());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,20 +214,29 @@ impl AutoScoreService {
|
|||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
for trigger in &rule.triggers {
|
for trigger in &rule.triggers {
|
||||||
if trigger.event == "interval_time_passed" {
|
if trigger.event != "interval_time_passed" {
|
||||||
let interval = parse_interval_trigger_value(trigger.value.as_ref());
|
continue;
|
||||||
let base_time = rule
|
|
||||||
.last_executed
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
|
|
||||||
.map(|value| value.with_timezone(&Utc))
|
|
||||||
.unwrap_or(now);
|
|
||||||
|
|
||||||
let next_execute_time = add_interval_to_time(base_time, &interval)?;
|
|
||||||
let delay_ms = (next_execute_time - now).num_milliseconds();
|
|
||||||
return Some(delay_ms.max(0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let interval = parse_interval_trigger_value(trigger.value.as_deref()).unwrap_or(
|
||||||
|
IntervalTriggerValue {
|
||||||
|
amount: DEFAULT_INTERVAL_MINUTES,
|
||||||
|
unit: IntervalUnit::Minute,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let base_time = rule
|
||||||
|
.last_executed
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
|
||||||
|
.map(|value| value.with_timezone(&Utc))
|
||||||
|
.unwrap_or(now);
|
||||||
|
|
||||||
|
let next_execute_time = add_interval_to_time(base_time, &interval)?;
|
||||||
|
let delay_ms = (next_execute_time - now).num_milliseconds();
|
||||||
|
return Some(delay_ms.max(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,27 +245,197 @@ impl AutoScoreService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_interval_trigger_value(value: Option<&String>) -> IntervalTriggerValue {
|
fn normalize_rule(mut rule: AutoScoreRule) -> Result<AutoScoreRule, String> {
|
||||||
if let Some(raw_value) = value {
|
rule.name = normalize_required_string(rule.name, "automation name")?;
|
||||||
if let Ok(minutes) = raw_value.parse::<i64>() {
|
rule.student_names = dedupe_trimmed_strings(rule.student_names);
|
||||||
if minutes > 0 {
|
|
||||||
return IntervalTriggerValue {
|
if rule.triggers.is_empty() {
|
||||||
amount: minutes,
|
return Err("At least one trigger is required".to_string());
|
||||||
unit: IntervalUnit::Minute,
|
}
|
||||||
};
|
if rule.actions.is_empty() {
|
||||||
}
|
return Err("At least one action is required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.triggers = rule
|
||||||
|
.triggers
|
||||||
|
.into_iter()
|
||||||
|
.map(normalize_trigger)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
rule.actions = rule
|
||||||
|
.actions
|
||||||
|
.into_iter()
|
||||||
|
.map(normalize_action)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
rule.last_executed = normalize_last_executed(rule.last_executed);
|
||||||
|
|
||||||
|
Ok(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_trigger(trigger: AutoScoreTrigger) -> Result<AutoScoreTrigger, String> {
|
||||||
|
let event = normalize_required_string(trigger.event, "trigger event")?;
|
||||||
|
|
||||||
|
match event.as_str() {
|
||||||
|
"interval_time_passed" => {
|
||||||
|
let interval = parse_interval_trigger_value(trigger.value.as_deref())
|
||||||
|
.ok_or_else(|| "Invalid interval trigger value".to_string())?;
|
||||||
|
let value = stringify_interval_trigger_value(&interval)
|
||||||
|
.ok_or_else(|| "Invalid interval trigger value".to_string())?;
|
||||||
|
|
||||||
|
Ok(AutoScoreTrigger {
|
||||||
|
event,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"student_has_tag" => {
|
||||||
|
let tag_values = parse_tag_values(trigger.value.as_deref());
|
||||||
|
let value = stringify_tag_values(&tag_values)
|
||||||
|
.ok_or_else(|| "Student tag trigger requires at least one tag".to_string())?;
|
||||||
|
|
||||||
|
Ok(AutoScoreTrigger {
|
||||||
|
event,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(format!("Unsupported trigger event: {}", event)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_action(action: AutoScoreAction) -> Result<AutoScoreAction, String> {
|
||||||
|
let event = normalize_required_string(action.event, "action event")?;
|
||||||
|
|
||||||
|
match event.as_str() {
|
||||||
|
"add_score" => {
|
||||||
|
let value = normalize_non_zero_numeric_string(action.value.as_deref())
|
||||||
|
.ok_or_else(|| "Add score action requires a non-zero numeric value".to_string())?;
|
||||||
|
|
||||||
|
Ok(AutoScoreAction {
|
||||||
|
event,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"add_tag" => {
|
||||||
|
let tag_values = parse_tag_values(action.value.as_deref());
|
||||||
|
let value = stringify_tag_values(&tag_values)
|
||||||
|
.ok_or_else(|| "Add tag action requires at least one tag".to_string())?;
|
||||||
|
|
||||||
|
Ok(AutoScoreAction {
|
||||||
|
event,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(format!("Unsupported action event: {}", event)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_required_string(value: String, field_name: &str) -> Result<String, String> {
|
||||||
|
let normalized = value.trim().to_string();
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return Err(format!("{} is required", field_name));
|
||||||
|
}
|
||||||
|
Ok(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dedupe_trimmed_strings(values: Vec<String>) -> Vec<String> {
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let mut normalized = Vec::new();
|
||||||
|
|
||||||
|
for value in values {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(parsed_value) = serde_json::from_str::<IntervalTriggerValue>(raw_value) {
|
let owned = trimmed.to_string();
|
||||||
if parsed_value.amount > 0 {
|
if seen.insert(owned.clone()) {
|
||||||
return parsed_value;
|
normalized.push(owned);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IntervalTriggerValue {
|
normalized
|
||||||
amount: 30,
|
}
|
||||||
unit: IntervalUnit::Minute,
|
|
||||||
|
fn parse_tag_values(value: Option<&str>) -> Vec<String> {
|
||||||
|
let Some(raw_value) = value.map(str::trim) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
if raw_value.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw_value.starts_with('[') {
|
||||||
|
if let Ok(parsed_values) = serde_json::from_str::<Vec<String>>(raw_value) {
|
||||||
|
return dedupe_trimmed_strings(parsed_values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dedupe_trimmed_strings(vec![raw_value.to_string()])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_tag_values(values: &[String]) -> Option<String> {
|
||||||
|
if values.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if values.len() == 1 {
|
||||||
|
return values.first().cloned();
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::to_string(values).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_non_zero_numeric_string(value: Option<&str>) -> Option<String> {
|
||||||
|
let normalized = value?.trim();
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = normalized.parse::<f64>().ok()?;
|
||||||
|
if !parsed.is_finite() || parsed == 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(normalized.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_last_executed(value: Option<String>) -> Option<String> {
|
||||||
|
value
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|raw_value| DateTime::parse_from_rfc3339(raw_value).ok())
|
||||||
|
.map(|parsed| parsed.with_timezone(&Utc).to_rfc3339())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_interval_trigger_value(value: Option<&str>) -> Option<IntervalTriggerValue> {
|
||||||
|
let raw_value = value.map(str::trim)?;
|
||||||
|
if raw_value.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(minutes) = raw_value.parse::<i64>() {
|
||||||
|
if minutes > 0 {
|
||||||
|
return Some(IntervalTriggerValue {
|
||||||
|
amount: minutes,
|
||||||
|
unit: IntervalUnit::Minute,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed_value = serde_json::from_str::<IntervalTriggerValue>(raw_value).ok()?;
|
||||||
|
if parsed_value.amount <= 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(parsed_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_interval_trigger_value(value: &IntervalTriggerValue) -> Option<String> {
|
||||||
|
if value.amount <= 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match value.unit {
|
||||||
|
IntervalUnit::Minute => Some(value.amount.to_string()),
|
||||||
|
IntervalUnit::Day | IntervalUnit::Month => serde_json::to_string(value).ok(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -67,7 +67,19 @@ impl AppState {
|
|||||||
theme.load_saved_theme(¤t_theme_id);
|
theme.load_saved_theme(¤t_theme_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.auto_score.write().initialize(&self.app_handle).await?;
|
let auto_score_rules = {
|
||||||
|
let settings = self.settings.read();
|
||||||
|
match settings.get_value(SettingsKey::AutoScoreRules) {
|
||||||
|
SettingsValue::Json(value) => value,
|
||||||
|
_ => serde_json::Value::Array(vec![]),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut auto_score = self.auto_score.write();
|
||||||
|
auto_score.load_rules(auto_score_rules);
|
||||||
|
auto_score.initialize(&self.app_handle).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
|
|||||||
{
|
{
|
||||||
title: t("autoScore.status"),
|
title: t("autoScore.status"),
|
||||||
key: "status",
|
key: "status",
|
||||||
width: 120,
|
width: 96,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Switch
|
<Switch
|
||||||
checked={row.enabled}
|
checked={row.enabled}
|
||||||
@@ -317,7 +317,8 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
|
|||||||
{
|
{
|
||||||
title: t("autoScore.applicableStudents"),
|
title: t("autoScore.applicableStudents"),
|
||||||
key: "studentNames",
|
key: "studentNames",
|
||||||
width: 220,
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
render: (_, row) =>
|
render: (_, row) =>
|
||||||
row.studentNames.length > 0 ? (
|
row.studentNames.length > 0 ? (
|
||||||
<Tag>{t("autoScore.studentCount", { count: row.studentNames.length })}</Tag>
|
<Tag>{t("autoScore.studentCount", { count: row.studentNames.length })}</Tag>
|
||||||
@@ -328,26 +329,27 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
|
|||||||
{
|
{
|
||||||
title: t("autoScore.triggers"),
|
title: t("autoScore.triggers"),
|
||||||
key: "triggers",
|
key: "triggers",
|
||||||
width: 150,
|
width: 120,
|
||||||
render: (_, row) => <Tag>{t("autoScore.triggerCount", { count: row.triggers.length })}</Tag>,
|
render: (_, row) => <Tag>{t("autoScore.triggerCount", { count: row.triggers.length })}</Tag>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("autoScore.actions"),
|
title: t("autoScore.actions"),
|
||||||
key: "actions",
|
key: "actions",
|
||||||
width: 140,
|
width: 120,
|
||||||
render: (_, row) => <Tag>{t("autoScore.actionCount", { count: row.actions.length })}</Tag>,
|
render: (_, row) => <Tag>{t("autoScore.actionCount", { count: row.actions.length })}</Tag>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("autoScore.lastExecuted"),
|
title: t("autoScore.lastExecuted"),
|
||||||
dataIndex: "lastExecuted",
|
dataIndex: "lastExecuted",
|
||||||
key: "lastExecuted",
|
key: "lastExecuted",
|
||||||
width: 180,
|
width: 160,
|
||||||
|
ellipsis: true,
|
||||||
render: (value: string | null | undefined) => formatLastExecuted(value),
|
render: (value: string | null | undefined) => formatLastExecuted(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("common.operation"),
|
title: t("common.operation"),
|
||||||
key: "operation",
|
key: "operation",
|
||||||
width: 160,
|
width: 140,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<Button type="link" disabled={!canEdit} onClick={() => handleEdit(row)}>
|
<Button type="link" disabled={!canEdit} onClick={() => handleEdit(row)}>
|
||||||
@@ -440,6 +442,8 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={pagedRules}
|
dataSource={pagedRules}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
|
tableLayout="fixed"
|
||||||
|
scroll={{ x: 880 }}
|
||||||
style={{ color: "var(--ss-text-main)" }}
|
style={{ color: "var(--ss-text-main)" }}
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: 16, textAlign: "right" }}>
|
<div style={{ marginTop: 16, textAlign: "right" }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user