mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -99,8 +99,8 @@ async def edit_field(session: SessionDep, field: CoreField):
|
||||
|
||||
|
||||
@router.post("/previewData/{id}")
|
||||
async def edit_local(session: SessionDep, id: int, data: TableObj):
|
||||
return preview(session, id, data)
|
||||
async def edit_local(session: SessionDep, current_user: CurrentUser, id: int, data: TableObj):
|
||||
return preview(session, current_user, id, data)
|
||||
|
||||
|
||||
@router.post("/fieldEnum/{id}")
|
||||
|
||||
@@ -237,11 +237,27 @@ def updateField(session: SessionDep, field: CoreField):
|
||||
update_field(session, field)
|
||||
|
||||
|
||||
def preview(session: SessionDep, id: int, data: TableObj):
|
||||
def preview(session: SessionDep, current_user: CurrentUser, id: int, data: TableObj):
|
||||
if data.fields is None or len(data.fields) == 0:
|
||||
return {"fields": [], "data": [], "sql": ''}
|
||||
|
||||
fields = [f.field_name for f in data.fields if f.checked]
|
||||
# column is checked, and, column permission for data.fields
|
||||
f_list = [f for f in data.fields if f.checked]
|
||||
column_permissions = session.query(DsPermission).filter(
|
||||
and_(DsPermission.table_id == data.table.id, DsPermission.type == 'column')).all()
|
||||
if column_permissions is not None:
|
||||
for permission in column_permissions:
|
||||
# check permission and user in same rules
|
||||
obj = session.query(DsRules).filter(
|
||||
and_(DsRules.permission_list.op('@>')(cast([permission.id], JSONB)),
|
||||
or_(DsRules.user_list.op('@>')(cast([f'{current_user.id}'], JSONB)),
|
||||
DsRules.user_list.op('@>')(cast([current_user.id], JSONB))))
|
||||
).first()
|
||||
if obj is not None:
|
||||
permission_list = json.loads(permission.permissions)
|
||||
f_list = filter_list(f_list, permission_list)
|
||||
|
||||
fields = [f.field_name for f in f_list]
|
||||
if fields is None or len(fields) == 0:
|
||||
return {"fields": [], "data": [], "sql": ''}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from sqlmodel import func, or_, select, delete as sqlmodel_delete
|
||||
from apps.system.crud.user import clean_user_cache, get_db_user, single_delete, user_ws_options
|
||||
from apps.system.crud.user import get_db_user, single_delete, user_ws_options
|
||||
from apps.system.models.system_model import UserWsModel
|
||||
from apps.system.models.user import UserModel
|
||||
from apps.system.schemas.auth import CacheName, CacheNamespace
|
||||
@@ -95,14 +95,13 @@ async def ws_options(session: SessionDep, current_user: CurrentUser, trans: Tran
|
||||
return await user_ws_options(session, current_user.id, trans)
|
||||
|
||||
@router.put("/ws/{oid}")
|
||||
# @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
|
||||
#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
|
||||
async def ws_change(session: SessionDep, current_user: CurrentUser, oid: int):
|
||||
ws_list: list[UserWs] = await user_ws_options(session, current_user.id)
|
||||
if not any(x.id == oid for x in ws_list):
|
||||
raise HTTPException(f"oid [{oid}] is invalid!")
|
||||
user_model: UserModel = get_db_user(session = session, user_id = current_user.id)
|
||||
user_model.oid = oid
|
||||
await clean_user_cache(user_model.id)
|
||||
session.add(user_model)
|
||||
session.commit()
|
||||
|
||||
@@ -181,7 +180,6 @@ async def langChange(session: SessionDep, current_user: CurrentUser, language: U
|
||||
return {"message": "Language not supported"}
|
||||
db_user: UserModel = get_db_user(session=session, user_id=current_user.id)
|
||||
db_user.language = lang
|
||||
await clean_user_cache(db_user.id)
|
||||
session.add(db_user)
|
||||
session.commit()
|
||||
|
||||
@@ -202,6 +200,5 @@ async def pwdUpdate(session: SessionDep, current_user: CurrentUser, editor: PwdE
|
||||
if not verify_md5pwd(editor.pwd, db_user.password):
|
||||
raise HTTPException("pwd error")
|
||||
db_user.password = md5pwd(editor.new_pwd)
|
||||
await clean_user_cache(db_user.id)
|
||||
session.add(db_user)
|
||||
session.commit()
|
||||
@@ -71,7 +71,7 @@ class TokenMiddleware(BaseHTTPMiddleware):
|
||||
) """
|
||||
return True, session_user
|
||||
except Exception as e:
|
||||
SQLBotLogUtil.exception(f"Token validation error: {str(e)}", exc_info=True)
|
||||
SQLBotLogUtil.exception(f"Token validation error: {str(e)}")
|
||||
return False, e
|
||||
|
||||
|
||||
@@ -97,6 +97,6 @@ class TokenMiddleware(BaseHTTPMiddleware):
|
||||
assistant_info = AssistantHeader.model_validate(assistant_info.model_dump(exclude_unset=True))
|
||||
return True, session_user, assistant_info
|
||||
except Exception as e:
|
||||
SQLBotLogUtil.exception(f"Assistant validation error: {str(e)}", exc_info=True)
|
||||
SQLBotLogUtil.exception(f"Assistant validation error: {str(e)}")
|
||||
# Return False and the exception message
|
||||
return False, e
|
||||
@@ -68,7 +68,7 @@ class ResponseMiddleware(BaseHTTPMiddleware):
|
||||
class exception_handler():
|
||||
@staticmethod
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
SQLBotLogUtil.exception(f"HTTP Exception: {exc.detail}", exc_info=True)
|
||||
SQLBotLogUtil.error(f"HTTP Exception: {exc.detail}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
@@ -82,7 +82,7 @@ class exception_handler():
|
||||
|
||||
@staticmethod
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
SQLBotLogUtil.exception(f"Unhandled Exception: {str(exc)}", exc_info=True)
|
||||
SQLBotLogUtil.error(f"Unhandled Exception: {str(exc)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
|
||||
+102
-108
@@ -1,35 +1,50 @@
|
||||
import contextvars
|
||||
from fastapi_cache import FastAPICache
|
||||
from fastapi_cache.decorator import cache as original_cache
|
||||
from functools import partial, wraps
|
||||
from typing import Optional, Set, Any, Dict, Tuple
|
||||
from inspect import Parameter, signature
|
||||
from typing import Optional, Any, Dict, Tuple
|
||||
from inspect import signature
|
||||
from contextlib import asynccontextmanager
|
||||
import asyncio
|
||||
import random
|
||||
from collections import defaultdict
|
||||
|
||||
from common.utils.utils import SQLBotLogUtil
|
||||
|
||||
|
||||
# 锁管理
|
||||
_cache_locks = {}
|
||||
# 使用contextvar来跟踪当前线程已持有的锁
|
||||
_held_locks = contextvars.ContextVar('held_locks', default=set())
|
||||
# 高效锁管理器
|
||||
class LockManager:
|
||||
_locks = defaultdict(asyncio.Lock)
|
||||
|
||||
@classmethod
|
||||
def get_lock(cls, key: str) -> asyncio.Lock:
|
||||
return cls._locks[key]
|
||||
|
||||
@asynccontextmanager
|
||||
async def _get_cache_lock(key: str):
|
||||
lock = _cache_locks.setdefault(key, asyncio.Lock())
|
||||
async with lock:
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if key in _cache_locks and not lock.locked():
|
||||
del _cache_locks[key]
|
||||
|
||||
def should_skip_param(param: Parameter) -> bool:
|
||||
"""判断参数是否应该被忽略(依赖注入参数)"""
|
||||
return (
|
||||
param.kind == Parameter.VAR_KEYWORD or # **kwargs
|
||||
param.kind == Parameter.VAR_POSITIONAL or # *args
|
||||
hasattr(param.annotation, "__module__") and
|
||||
param.annotation.__module__.startswith(('fastapi', 'starlette', "sqlmodel.orm.session"))
|
||||
)
|
||||
# 获取当前已持有的锁集合
|
||||
current_locks = _held_locks.get()
|
||||
|
||||
# 如果已经持有这个锁,直接yield(锁传递)
|
||||
if key in current_locks:
|
||||
yield
|
||||
return
|
||||
|
||||
# 否则获取锁并添加到当前上下文中
|
||||
lock = LockManager.get_lock(key)
|
||||
try:
|
||||
await lock.acquire()
|
||||
# 更新当前持有的锁集合
|
||||
new_locks = current_locks | {key}
|
||||
token = _held_locks.set(new_locks)
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# 恢复之前的锁集合
|
||||
_held_locks.reset(token)
|
||||
if lock.locked():
|
||||
lock.release()
|
||||
|
||||
def custom_key_builder(
|
||||
func: Any,
|
||||
@@ -37,137 +52,116 @@ def custom_key_builder(
|
||||
*,
|
||||
args: Tuple[Any, ...] = (),
|
||||
kwargs: Dict[str, Any],
|
||||
additional_skip_args: Optional[Set[str]] = None,
|
||||
cacheName: Optional[str] = None,
|
||||
cacheName: str,
|
||||
keyExpression: Optional[str] = None,
|
||||
) -> str:
|
||||
"""完全兼容FastAPICache的键生成器"""
|
||||
if cacheName:
|
||||
try:
|
||||
base_key = f"{namespace}:{cacheName}:"
|
||||
|
||||
if keyExpression:
|
||||
try:
|
||||
sig = signature(func)
|
||||
bound_args = sig.bind_partial(*args, **kwargs)
|
||||
bound_args.apply_defaults()
|
||||
|
||||
if keyExpression.startswith("args["):
|
||||
import re
|
||||
match = re.match(r"args\[(\d+)\]", keyExpression)
|
||||
if match:
|
||||
index = int(match.group(1))
|
||||
value = bound_args.args[index]
|
||||
base_key += f"{value}:"
|
||||
else:
|
||||
parts = keyExpression.split('.')
|
||||
value = bound_args.arguments[parts[0]]
|
||||
for part in parts[1:]:
|
||||
value = getattr(value, part)
|
||||
base_key += f"{value}:"
|
||||
|
||||
except (IndexError, KeyError, AttributeError) as e:
|
||||
SQLBotLogUtil.warning(f"Failed to evaluate keyExpression '{keyExpression}': {str(e)}")
|
||||
sig = signature(func)
|
||||
bound_args = sig.bind_partial(*args, **kwargs)
|
||||
bound_args.apply_defaults()
|
||||
|
||||
# 支持args[0]格式
|
||||
if keyExpression.startswith("args["):
|
||||
import re
|
||||
if match := re.match(r"args\[(\d+)\]", keyExpression):
|
||||
index = int(match.group(1))
|
||||
value = bound_args.args[index]
|
||||
return f"{base_key}{value}"
|
||||
|
||||
# 支持属性路径格式
|
||||
parts = keyExpression.split('.')
|
||||
value = bound_args.arguments[parts[0]]
|
||||
for part in parts[1:]:
|
||||
value = getattr(value, part)
|
||||
return f"{base_key}{value}"
|
||||
|
||||
return base_key
|
||||
|
||||
sig = signature(func)
|
||||
auto_skip_args = {
|
||||
name for name, param in sig.parameters.items()
|
||||
if should_skip_param(param)
|
||||
}
|
||||
skip_args = auto_skip_args.union(additional_skip_args or set())
|
||||
filtered_kwargs = {
|
||||
k: v for k, v in kwargs.items() if k not in skip_args
|
||||
}
|
||||
|
||||
bound_args = sig.bind_partial(*args, **kwargs)
|
||||
bound_args.apply_defaults()
|
||||
|
||||
filtered_args = []
|
||||
for i, (name, value) in enumerate(bound_args.arguments.items()):
|
||||
if i < len(args) and name not in skip_args:
|
||||
filtered_args.append(value)
|
||||
filtered_args = tuple(filtered_args)
|
||||
|
||||
default_key_builder = FastAPICache.get_key_builder()
|
||||
return default_key_builder(
|
||||
func=func,
|
||||
namespace=namespace,
|
||||
args=filtered_args,
|
||||
kwargs=filtered_kwargs,
|
||||
)
|
||||
# 默认使用第一个参数作为key
|
||||
return f"{base_key}{args[0] if args else 'default'}"
|
||||
|
||||
except Exception as e:
|
||||
SQLBotLogUtil.error(f"Key builder error: {str(e)}")
|
||||
raise ValueError(f"Invalid cache key generation: {e}") from e
|
||||
|
||||
def cache(
|
||||
expire: Optional[int] = 60 * 60 * 24,
|
||||
namespace: Optional[str] = None,
|
||||
key_builder: Optional[Any] = None,
|
||||
expire: int = 60 * 60 * 24,
|
||||
namespace: str = "",
|
||||
*,
|
||||
additional_skip_args: Optional[Set[str]] = None,
|
||||
cacheName: Optional[str] = None,
|
||||
cacheName: str, # 必须提供cacheName
|
||||
keyExpression: Optional[str] = None,
|
||||
jitter: int = 60, # 默认抖动60秒
|
||||
):
|
||||
"""完全兼容的缓存装饰器"""
|
||||
def decorator(func):
|
||||
if key_builder is None:
|
||||
used_key_builder = partial(
|
||||
custom_key_builder,
|
||||
additional_skip_args=additional_skip_args,
|
||||
cacheName=cacheName,
|
||||
keyExpression=keyExpression
|
||||
)
|
||||
else:
|
||||
used_key_builder = key_builder
|
||||
|
||||
# 预先生成key builder
|
||||
used_key_builder = partial(
|
||||
custom_key_builder,
|
||||
cacheName=cacheName,
|
||||
keyExpression=keyExpression
|
||||
)
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
# 生成缓存键
|
||||
cache_key = used_key_builder(
|
||||
func=func,
|
||||
namespace=namespace or "",
|
||||
namespace=namespace,
|
||||
args=args,
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
# 防击穿锁
|
||||
async with _get_cache_lock(cache_key):
|
||||
SQLBotLogUtil.info(f"Using cache key: {cache_key}")
|
||||
backend = FastAPICache.get_backend()
|
||||
cached_value = await backend.get(cache_key)
|
||||
if cached_value is not None:
|
||||
SQLBotLogUtil.info(f"Cache hit for key: {cache_key}, the value is: {cached_value}")
|
||||
return cached_value
|
||||
|
||||
# 双重检查
|
||||
if (cached := await backend.get(cache_key)) is not None:
|
||||
SQLBotLogUtil.debug(f"Cache hit: {cache_key}")
|
||||
return cached
|
||||
|
||||
# 执行函数并缓存结果
|
||||
result = await func(*args, **kwargs)
|
||||
await backend.set(cache_key, result, expire)
|
||||
SQLBotLogUtil.info(f"Cache miss for key: {cache_key}, result cached.")
|
||||
|
||||
actual_expire = expire + random.randint(-jitter, jitter)
|
||||
await backend.set(cache_key, result, actual_expire)
|
||||
|
||||
SQLBotLogUtil.debug(f"Cache set: {cache_key} (expire: {actual_expire}s)")
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def clear_cache(
|
||||
namespace: Optional[str] = None,
|
||||
cacheName: Optional[str] = None,
|
||||
namespace: str = "",
|
||||
*,
|
||||
cacheName: str,
|
||||
keyExpression: Optional[str] = None,
|
||||
):
|
||||
"""清除缓存的装饰器"""
|
||||
"""精确清除单个缓存项的装饰器"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
cache_key = custom_key_builder(
|
||||
func=func,
|
||||
namespace=namespace or "",
|
||||
namespace=namespace,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
cacheName=cacheName,
|
||||
keyExpression=keyExpression,
|
||||
)
|
||||
|
||||
# 加锁防止竞争
|
||||
async with _get_cache_lock(cache_key):
|
||||
if await FastAPICache.get_backend().get(cache_key):
|
||||
await FastAPICache.clear(key=cache_key)
|
||||
backend = FastAPICache.get_backend()
|
||||
result = None
|
||||
if await backend.get(cache_key):
|
||||
await backend.clear(cache_key)
|
||||
result = await func(*args, **kwargs)
|
||||
SQLBotLogUtil.info(f"Clearing cache for key: {cache_key}")
|
||||
return result
|
||||
return await func(*args, **kwargs)
|
||||
if await backend.get(cache_key):
|
||||
await backend.clear(cache_key)
|
||||
SQLBotLogUtil.info(f"Cache cleared: {cache_key}")
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -341,7 +341,7 @@
|
||||
"there_are": "There are",
|
||||
"2_dashboards": "{msg} dashboards",
|
||||
"smart_data_centers": "{msg} smart data centers",
|
||||
"confirm_to_delete": "This data source is in use. Deleting it will cause these resources to not work properly. Confirm to delete?",
|
||||
"confirm_to_delete": "After the data source is deleted, the related questions can no longer be asked and the charts in the dashboard cannot be displayed normally. Please operate with caution!",
|
||||
"id_account_to_add": "Search user ID/account to add",
|
||||
"find_user": "Find user",
|
||||
"add_successfully": "Add successfully",
|
||||
|
||||
@@ -354,7 +354,7 @@
|
||||
"there_are": "有 ",
|
||||
"2_dashboards": "{msg} 个仪表板",
|
||||
"smart_data_centers": "{msg} 个智能问数",
|
||||
"confirm_to_delete": "正在使用此数据源,删除后会导致这些资源无法正常使用,确认删除?",
|
||||
"confirm_to_delete": "数据源删除后,相关的问数不可继续提问,仪表板中的图表无法正常显示,请谨慎操作!",
|
||||
"id_account_to_add": "搜索用户ID/账号添加",
|
||||
"find_user": "查找用户",
|
||||
"add_successfully": "添加成功",
|
||||
|
||||
@@ -308,6 +308,36 @@ class HttpService {
|
||||
}
|
||||
|
||||
public loadRemoteScript(url: string, id?: string, cb?: any): Promise<HTMLElement> {
|
||||
if (!url) {
|
||||
return Promise.reject(new Error('URL is required to load remote script'))
|
||||
}
|
||||
if (id && document.getElementById(id)) {
|
||||
return Promise.resolve(document.getElementById(id) as HTMLElement)
|
||||
}
|
||||
if (url.startsWith('/')) {
|
||||
const real_url = import.meta.env.VITE_API_BASE_URL.replace('/api/v1', '')
|
||||
url = real_url + url
|
||||
}
|
||||
return new Promise<HTMLElement>((resolve, reject) => {
|
||||
// 改用传统的script标签加载方式
|
||||
const script = document.createElement('script')
|
||||
script.src = url
|
||||
script.id = id || `remote-script-${Date.now()}`
|
||||
|
||||
script.onload = () => {
|
||||
if (cb) cb()
|
||||
resolve(script)
|
||||
}
|
||||
|
||||
script.onerror = (error) => {
|
||||
console.error(`Failed to load script from ${url}:`, error)
|
||||
reject(new Error(`Failed to load script from ${url}`))
|
||||
}
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
/* public loadRemoteScript(url: string, id?: string, cb?: any): Promise<HTMLElement> {
|
||||
if (!url) {
|
||||
return Promise.reject(new Error('URL is required to load remote script'))
|
||||
}
|
||||
@@ -337,7 +367,7 @@ class HttpService {
|
||||
reject(new Error(`Failed to load script from ${url}: ${error.message}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
|
||||
@@ -57,11 +57,11 @@ const onClickOutside = () => {
|
||||
<div class="name-icon">
|
||||
<img :src="icon" width="32px" height="32px" />
|
||||
<div class="info">
|
||||
<div class="name">{{ name }}</div>
|
||||
<div :title="name" class="name ellipsis">{{ name }}</div>
|
||||
<div class="type">{{ typeName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="type-value">
|
||||
<div :title="description" class="type-value">
|
||||
{{ description }}
|
||||
</div>
|
||||
|
||||
@@ -140,6 +140,7 @@ const onClickOutside = () => {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
max-width: 250px;
|
||||
}
|
||||
.type {
|
||||
font-weight: 400;
|
||||
|
||||
@@ -58,7 +58,7 @@ const keywords = ref('')
|
||||
const tableListWithSearch = computed(() => {
|
||||
if (!keywords.value) return tableList.value
|
||||
return tableList.value.filter((ele) =>
|
||||
ele.name.toLowerCase().includes(keywords.value.toLowerCase())
|
||||
ele.table_name.toLowerCase().includes(keywords.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
const total = ref(1000)
|
||||
@@ -181,6 +181,31 @@ const emits = defineEmits(['back', 'refresh'])
|
||||
const back = () => {
|
||||
emits('back')
|
||||
}
|
||||
|
||||
const btnSelectClick = (val: any) => {
|
||||
btnSelect.value = val
|
||||
loading.value = true
|
||||
|
||||
if (val === 'd') {
|
||||
datasourceApi
|
||||
.fieldList(currentTable.value.id)
|
||||
.then((res) => {
|
||||
fieldList.value = res
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
} else {
|
||||
datasourceApi
|
||||
.previewData(props.info.id, buildData())
|
||||
.then((res) => {
|
||||
previewData.value = res
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -217,24 +242,26 @@ const back = () => {
|
||||
</el-input>
|
||||
|
||||
<div v-loading="initLoading" class="list-content">
|
||||
<div
|
||||
v-for="ele in tableListWithSearch"
|
||||
:key="ele.table_name"
|
||||
class="model"
|
||||
:class="currentTable.table_name === ele.table_name && 'isActive'"
|
||||
:title="ele.table_name"
|
||||
@click="clickTable(ele)"
|
||||
>
|
||||
<el-icon size="16">
|
||||
<icon_form_outlined></icon_form_outlined>
|
||||
</el-icon>
|
||||
<span class="name">{{ ele.table_name }}</span>
|
||||
</div>
|
||||
<el-scrollbar v-if="tableListWithSearch.length">
|
||||
<div
|
||||
v-for="ele in tableListWithSearch"
|
||||
:key="ele.table_name"
|
||||
class="model"
|
||||
:class="currentTable.table_name === ele.table_name && 'isActive'"
|
||||
:title="ele.table_name"
|
||||
@click="clickTable(ele)"
|
||||
>
|
||||
<el-icon size="16">
|
||||
<icon_form_outlined></icon_form_outlined>
|
||||
</el-icon>
|
||||
<span class="name">{{ ele.table_name }}</span>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<EmptyBackground
|
||||
v-if="!!keywords && !tableListWithSearch.length"
|
||||
:description="$t('datasource.relevant_content_found')"
|
||||
img-type="tree"
|
||||
style="width: 100%; margin-top: 100px"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div v-else-if="!initLoading && !tableListWithSearch.length" class="no-data">
|
||||
<div class="no-data-msg">
|
||||
@@ -264,10 +291,18 @@ const back = () => {
|
||||
</div>
|
||||
<div class="table-content">
|
||||
<div class="btn-select">
|
||||
<el-button :class="[btnSelect === 'd' && 'is-active']" text @click="btnSelect = 'd'">
|
||||
<el-button
|
||||
:class="[btnSelect === 'd' && 'is-active']"
|
||||
text
|
||||
@click="btnSelectClick('d')"
|
||||
>
|
||||
{{ t('ds.table_schema') }}
|
||||
</el-button>
|
||||
<el-button :class="[btnSelect === 'q' && 'is-active']" text @click="btnSelect = 'q'">
|
||||
<el-button
|
||||
:class="[btnSelect === 'q' && 'is-active']"
|
||||
text
|
||||
@click="btnSelectClick('q')"
|
||||
>
|
||||
{{ t('ds.preview') }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -312,7 +347,7 @@ const back = () => {
|
||||
<div class="preview-num">
|
||||
{{ t('ds.pieces_in_total', { msg: total, ms: showNum }) }}
|
||||
</div>
|
||||
<el-table :data="previewData.data" style="width: 100%; height: 600px">
|
||||
<el-table :data="previewData.data" style="width: 100%">
|
||||
<el-table-column
|
||||
v-for="(c, index) in previewData.fields"
|
||||
:key="index"
|
||||
@@ -431,6 +466,14 @@ const back = () => {
|
||||
|
||||
.list-content {
|
||||
height: calc(100% - 100px);
|
||||
.no-result {
|
||||
margin-top: 72px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
color: #646a73;
|
||||
}
|
||||
.model {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
@@ -19,13 +19,6 @@ defineProps({
|
||||
})
|
||||
|
||||
const emits = defineEmits(['panelClick', 'smartClick'])
|
||||
const panelClick = () => {
|
||||
emits('panelClick')
|
||||
}
|
||||
|
||||
const smartClick = () => {
|
||||
emits('smartClick')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -34,9 +27,6 @@ const smartClick = () => {
|
||||
{{ t('datasource.data_source', { msg: name }) }}
|
||||
</div>
|
||||
<div class="use-panel">
|
||||
{{ t('workspace.there_are') }}
|
||||
<span @click="panelClick">{{ t('workspace.2_dashboards', { msg: panelNum }) }}</span
|
||||
>、<span @click="smartClick">{{ t('workspace.smart_data_centers', { msg: smartNum }) }}</span>
|
||||
{{ t('workspace.confirm_to_delete') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -714,7 +714,7 @@ const saveHandler = () => {
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="private-list_form" prop="private_list">
|
||||
<el-form-item class="private-list_form">
|
||||
<template #label>
|
||||
<div class="private-list">
|
||||
{{ t('embedded.set_data_source') }}
|
||||
|
||||
@@ -70,7 +70,7 @@ const deleteBatchUser = () => {
|
||||
})
|
||||
}
|
||||
const deleteHandler = (row: any) => {
|
||||
if (row.weight === 1) return
|
||||
if (row.weight === 1 && +userStore.getUid !== 1) return
|
||||
ElMessageBox.confirm(t('workspace.member_feng_yibudao', { msg: row.name }), {
|
||||
confirmButtonType: 'danger',
|
||||
confirmButtonText: t('dashboard.delete'),
|
||||
@@ -177,6 +177,7 @@ const addWorkspace = () => {
|
||||
}
|
||||
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageInfo.currentPage = 1
|
||||
pageInfo.pageSize = val
|
||||
search()
|
||||
}
|
||||
@@ -261,7 +262,7 @@ const handleCurrentChange = (val: number) => {
|
||||
>
|
||||
<el-icon
|
||||
class="action-btn"
|
||||
:class="scope.row.weight === 1 && 'not-allow'"
|
||||
:class="+userStore.getUid !== 1 && scope.row.weight === 1 && 'not-allow'"
|
||||
size="16"
|
||||
@click="deleteHandler(scope.row)"
|
||||
>
|
||||
@@ -286,18 +287,18 @@ const handleCurrentChange = (val: number) => {
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pageInfo.currentPage"
|
||||
v-model:page-size="pageInfo.pageSize"
|
||||
:page-sizes="[10, 20, 30]"
|
||||
:background="true"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pageInfo.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pageInfo.currentPage"
|
||||
v-model:page-size="pageInfo.pageSize"
|
||||
:page-sizes="[10, 20, 30]"
|
||||
:background="true"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pageInfo.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="multipleSelectionAll.length" class="bottom-select">
|
||||
<el-checkbox
|
||||
@@ -369,15 +370,15 @@ const handleCurrentChange = (val: number) => {
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.table-content {
|
||||
height: calc(100% - 104px);
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
max-height: calc(100% - 104px);
|
||||
overflow-y: auto;
|
||||
|
||||
.preview-or-schema {
|
||||
.user-status-container {
|
||||
|
||||
@@ -256,7 +256,7 @@ const submit = (item: any) => {
|
||||
@click="handleDefaultModelChange(ele)"
|
||||
>
|
||||
<img :src="ele.supplier_item.icon" width="24px" height="24px" />
|
||||
<div class="model-name" v-html="formatKeywords(ele.name)"></div>
|
||||
<div class="model-name ellipsis" v-html="formatKeywords(ele.name)"></div>
|
||||
<el-icon size="16" class="done">
|
||||
<icon_done_outlined></icon_done_outlined>
|
||||
</el-icon>
|
||||
@@ -420,6 +420,7 @@ const submit = (item: any) => {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.done {
|
||||
|
||||
@@ -66,7 +66,12 @@
|
||||
<el-table-column prop="email" show-overflow-tooltip :label="$t('user.email')" />
|
||||
<!-- <el-table-column prop="phone" :label="$t('user.phone_number')" width="280" /> -->
|
||||
<!-- <el-table-column prop="user_source" :label="$t('user.user_source')" width="280" /> -->
|
||||
<el-table-column prop="oid_list" :label="$t('user.workspace')" width="280">
|
||||
<el-table-column
|
||||
show-overflow-tooltip
|
||||
prop="oid_list"
|
||||
:label="$t('user.workspace')"
|
||||
width="280"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ formatSpaceName(scope.row.oid_list) }}</span>
|
||||
</template>
|
||||
@@ -671,6 +676,7 @@ const saveHandler = () => {
|
||||
}
|
||||
const handleSizeChange = (val: number) => {
|
||||
state.pageInfo.pageSize = val
|
||||
state.pageInfo.currentPage = 1
|
||||
search()
|
||||
}
|
||||
const handleCurrentChange = (val: number) => {
|
||||
|
||||
@@ -238,6 +238,7 @@ const saveHandler = () => {
|
||||
}
|
||||
}
|
||||
const handleSizeChange = (val: number) => {
|
||||
state.pageInfo.currentPage = 1
|
||||
state.pageInfo.pageSize = val
|
||||
search()
|
||||
}
|
||||
|
||||
@@ -171,6 +171,10 @@ defineExpose({
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ed-checkbox {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@ const addWorkspace = (row: any) => {
|
||||
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageInfo.pageSize = val
|
||||
pageInfo.currentPage = 1
|
||||
search()
|
||||
}
|
||||
const handleCurrentChange = (val: number) => {
|
||||
|
||||
Reference in New Issue
Block a user