Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangjiahao
2025-07-24 12:11:03 +08:00
20 changed files with 701 additions and 220 deletions
+21 -18
View File
@@ -28,6 +28,7 @@ from apps.chat.curd.chat import save_question, save_full_sql_message, save_full_
get_old_questions, save_analysis_predict_record, list_base_records, rename_chat
from apps.chat.models.chat_model import ChatQuestion, ChatRecord, Chat, RenameChat
from apps.datasource.crud.datasource import get_table_schema
from apps.datasource.crud.datasource import is_normal_user
from apps.datasource.crud.row_permission import transFilterTree
from apps.datasource.models.datasource import CoreDatasource, CoreTable
from apps.db.db import exec_sql
@@ -791,26 +792,28 @@ class LLMService:
SQLBotLogUtil.info(full_sql_text)
# todo row permission
sql_json_str = extract_nested_json(full_sql_text)
data = orjson.loads(sql_json_str)
if is_normal_user(self.current_user):
sql_json_str = extract_nested_json(full_sql_text)
data = orjson.loads(sql_json_str)
sql = ''
message = ''
error = False
if data['success']:
sql = data['sql']
sql = ''
message = ''
error = False
if data['success']:
sql = data['sql']
else:
message = data['message']
error = True
if error:
raise Exception(message)
if sql.strip() == '':
raise Exception("SQL query is empty")
sql_result = self.generate_filter(data.get('sql'), data.get('tables')) # maybe no sql and tables
SQLBotLogUtil.info(sql_result)
sql = self.check_save_sql(res=sql_result)
else:
message = data['message']
error = True
if error:
raise Exception(message)
if sql.strip() == '':
raise Exception("SQL query is empty")
sql_result = self.generate_filter(data.get('sql'), data.get('tables')) # maybe no sql and tables
SQLBotLogUtil.info(sql_result)
sql = self.check_save_sql(res=sql_result)
# sql = llm_service.check_save_sql(res=full_sql_text)
sql = self.check_save_sql(res=full_sql_text)
SQLBotLogUtil.info(sql)
format_sql = sqlparse.format(sql, reindent=True)
+65 -32
View File
@@ -5,10 +5,12 @@ from typing import List
from fastapi import HTTPException
from sqlalchemy import and_, text, cast, or_, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlbot_xpack.permissions.models.ds_permission import DsPermission
from sqlbot_xpack.permissions.api.permission import transRecord2DTO
from sqlbot_xpack.permissions.models.ds_permission import DsPermission, PermissionDTO
from sqlbot_xpack.permissions.models.ds_rules import DsRules
from sqlmodel import select
from apps.datasource.crud.row_permission import transFilterTree
from apps.datasource.utils.utils import aes_decrypt
from apps.db.constant import DB
from apps.db.db import get_engine, get_tables, get_fields, exec_sql
@@ -238,43 +240,69 @@ def updateField(session: SessionDep, field: CoreField):
def preview(session: SessionDep, current_user: CurrentUser, id: int, data: TableObj):
ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first()
check_status(session, ds, True)
if data.fields is None or len(data.fields) == 0:
return {"fields": [], "data": [], "sql": ''}
# column is checked, and, column permission for data.fields
where = None
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)
if is_normal_user(current_user):
# column is checked, and, column permission for data.fields
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)
# row permission tree
row_permissions = session.query(DsPermission).filter(
and_(DsPermission.table_id == data.table.id, DsPermission.type == 'row')).all()
res: List[PermissionDTO] = []
if row_permissions is not None:
for permission in row_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:
res.append(transRecord2DTO(session, permission))
wheres = transFilterTree(session, res, ds)
where = (' where ' + wheres) if wheres is not None and wheres != '' else ''
fields = [f.field_name for f in f_list]
if fields is None or len(fields) == 0:
return {"fields": [], "data": [], "sql": ''}
ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first()
check_status(session, ds, True)
conf = DatasourceConf(**json.loads(aes_decrypt(ds.configuration))) if ds.type != "excel" else get_engine_config()
sql: str = ""
if ds.type == "mysql":
sql = f"""SELECT `{"`, `".join(fields)}` FROM `{data.table.table_name}` LIMIT 100"""
sql = f"""SELECT `{"`, `".join(fields)}` FROM `{data.table.table_name}`
{where}
LIMIT 100"""
elif ds.type == "sqlServer":
sql = f"""SELECT [{"], [".join(fields)}] FROM [{conf.dbSchema}].[{data.table.table_name}]
{where}
ORDER BY [{data.fields[0].field_name}]
OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY"""
elif ds.type == "pg" or ds.type == "excel":
sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}" LIMIT 100"""
sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}"
{where}
LIMIT 100"""
elif ds.type == "oracle":
sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}"
{where}
ORDER BY "{data.fields[0].field_name}"
OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY"""
return exec_sql(ds, sql)
@@ -320,19 +348,20 @@ def get_table_obj_by_ds(session: SessionDep, current_user: CurrentUser, ds: Core
fields = session.query(CoreField).filter(and_(CoreField.table_id == table.id, CoreField.checked == True)).all()
# do column permissions, filter fields
column_permissions = session.query(DsPermission).filter(
and_(DsPermission.table_id == 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)
fields = filter_list(fields, permission_list)
if is_normal_user(current_user):
column_permissions = session.query(DsPermission).filter(
and_(DsPermission.table_id == 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)
fields = filter_list(fields, permission_list)
_list.append(TableAndFields(schema=schema, table=table, fields=fields))
return _list
@@ -376,3 +405,7 @@ def filter_list(list_a, list_b):
id_to_invalid[b['field_id']] = True
return [a for a in list_a if not id_to_invalid.get(a.id, False)]
def is_normal_user(current_user: CurrentUser):
return current_user.id != 1 and (current_user.weight is not None and current_user.weight != 1)
@@ -3,8 +3,10 @@
from typing import List, Dict
from apps.datasource.models.datasource import CoreField, CoreDatasource
from sqlbot_xpack.permissions.models.ds_permission import PermissionTree, PermissionDTO
from apps.datasource.models.datasource import CoreField, CoreDatasource
from apps.db.constant import DB
from common.core.deps import SessionDep
@@ -48,7 +50,8 @@ def transTreeItem(session: SessionDep, item: Dict, ds: CoreDatasource) -> str |
if field is None:
return None
whereName = field.field_name
db = DB.get_db(ds.type)
whereName = db.prefix + field.field_name + db.suffix
if item['filter_type'] == 'enum':
if len(item['enum_value']) > 0:
if ds['type'] == 'sqlServer' and (
+5 -5
View File
@@ -95,7 +95,7 @@ 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):
@@ -137,7 +137,7 @@ async def create(session: SessionDep, creator: UserCreator):
session.commit()
@router.put("")
#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="editor.id")
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="editor.id")
async def update(session: SessionDep, editor: UserEditor):
user_model: UserModel = get_db_user(session = session, user_id = editor.id)
origin_oid: int = user_model.oid
@@ -173,7 +173,7 @@ async def batch_del(session: SessionDep, id_list: list[int]):
await single_delete(session, id)
@router.put("/language")
#@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 langChange(session: SessionDep, current_user: CurrentUser, language: UserLanguage):
lang = language.language
if lang not in ["zh-CN", "en"]:
@@ -184,7 +184,7 @@ async def langChange(session: SessionDep, current_user: CurrentUser, language: U
session.commit()
@router.patch("/pwd/{id}")
#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int):
if not current_user.isAdmin:
raise HTTPException('only for admin')
@@ -194,7 +194,7 @@ async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int):
session.commit()
@router.put("/pwd")
#@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 pwdUpdate(session: SessionDep, current_user: CurrentUser, editor: PwdEditor):
db_user: UserModel = get_db_user(session=session, user_id=current_user.id)
if not verify_md5pwd(editor.pwd, db_user.password):
+3 -3
View File
@@ -22,7 +22,7 @@ def get_user_by_account(*, session: Session, account: str) -> BaseUserDTO | None
return None
return BaseUserDTO.model_validate(db_user.model_dump())
#@cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="user_id")
@cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="user_id")
async def get_user_info(*, session: Session, user_id: int) -> UserInfoDTO | None:
db_user: UserModel = get_db_user(session = session, user_id = user_id)
userInfo = UserInfoDTO.model_validate(db_user.model_dump())
@@ -58,7 +58,7 @@ async def user_ws_options(session: Session, uid: int, trans: Optional[I18n] = No
for id, name in result.all()
]
#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
async def single_delete(session: SessionDep, id: int):
user_model: UserModel = get_db_user(session = session, user_id = id)
del_stmt = sqlmodel_delete(UserWsModel).where(UserWsModel.uid == id)
@@ -66,6 +66,6 @@ async def single_delete(session: SessionDep, id: int):
session.delete(user_model)
session.commit()
#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
async def clean_user_cache(id: int):
SQLBotLogUtil.info(f"User cache for [{id}] has been cleaned")
+2
View File
@@ -124,6 +124,8 @@ def cache(
result = await func(*args, **kwargs)
actual_expire = expire + random.randint(-jitter, jitter)
if await backend.get(cache_key):
await backend.clear(cache_key)
await backend.set(cache_key, result, actual_expire)
SQLBotLogUtil.debug(f"Cache set: {cache_key} (expire: {actual_expire}s)")
@@ -6,13 +6,11 @@ import icon_done_outlined from '@/assets/svg/icon_done_outlined.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { userApi } from '@/api/auth'
import { ElMessage } from 'element-plus-secondary'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { t } = useI18n()
const router = useRouter()
defineProps({
collapse: { type: [Boolean], required: true },
})
@@ -46,7 +44,6 @@ const handleDefaultWorkspaceChange = (item: any) => {
currentWorkspace.value = { id: item.id, name: item.name }
userApi.ws_change(item.id).then(() => {
ElMessage.success(t('common.switch_success'))
router.push('/chat/index')
setTimeout(() => {
location.reload()
}, 300)
-32
View File
@@ -114,38 +114,6 @@ const router = createRouter({
component: DashboardPreview,
meta: { title: 'DashboardPreview', icon: 'dashboard' },
},
/* {
path: "/setting",
component: Layout,
redirect: "/setting/index",
children: [
{
path: "index",
name: "setting",
component: setting,
meta: { title: "Settings", icon: "setting" },
},
],
}, */
// {
// path: '/system',
// component: Layout,
// redirect: '/system/model',
// children: [
// /* {
// path: "user",
// name: "user",
// component: User,
// meta: { title: "User Management", icon: "icon_user" },
// }, */
// {
// path: 'model',
// name: 'model',
// component: Model,
// meta: { title: 'AI Model Configuration', icon: 'icon_ai' },
// },
// ],
// },
{
path: '/system',
component: LayoutDsl,
+9 -4
View File
@@ -26,10 +26,7 @@ export const watchRouter = (router: any) => {
if (!userStore.getUid) {
await userStore.info()
}
/* if (!wsCache.get('user.uid')) {
await userStore.info()
} */
if (to.path === '/') {
if (to.path === '/' || accessCrossPermission(to)) {
next('/chat')
return
}
@@ -41,6 +38,14 @@ export const watchRouter = (router: any) => {
}
})
}
const accessCrossPermission = (to: any) => {
if (!to?.path) return false
return (
(to.path.startsWith('/system') && !userStore.isAdmin) ||
(to.path.startsWith('/set') && !userStore.isSpaceAdmin)
)
}
const loadXpackStatic = () => {
if (document.getElementById('sqlbot_xpack_static')) {
return Promise.resolve()
+4
View File
@@ -164,6 +164,10 @@ body {
.tip {
margin-top: 24px;
}
.ed-message-box__message {
width: 95%;
}
}
.form-content_error {
+135 -83
View File
@@ -1,8 +1,13 @@
<script setup lang="ts">
<script lang="ts" setup>
import { onMounted, ref, computed, shallowRef } from 'vue'
import icon_close_outlined from '@/assets/svg/operate/ope-close.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import { useRouter } from 'vue-router'
import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { chatApi, ChatInfo } from '@/api/chat.ts'
import { onMounted, ref } from 'vue'
import { datasourceApi } from '@/api/datasource.ts'
import DatasourceItemCard from '../ds/DatasourceItemCard.vue'
import Card from '@/views/ds/ChatCard.vue'
const props = withDefaults(
defineProps<{
@@ -13,32 +18,48 @@ const props = withDefaults(
}
)
const dsList = ref<Array<any>>([])
const router = useRouter()
const searchLoading = ref(false)
const datasourceConfigvVisible = ref(false)
const keywords = ref('')
const datasourceList = shallowRef([] as any[])
const datasourceListWithSearch = computed(() => {
if (!keywords.value) return datasourceList.value
return datasourceList.value.filter((ele) =>
ele.name.toLowerCase().includes(keywords.value.toLowerCase())
)
})
const beforeClose = () => {
datasourceConfigvVisible.value = false
keywords.value = ''
}
const emits = defineEmits(['onChatCreated'])
function listDs() {
datasourceApi.list().then((res) => {
dsList.value = res
})
searchLoading.value = true
datasourceApi
.list()
.then((res) => {
datasourceList.value = res
})
.finally(() => {
searchLoading.value = false
})
}
const dialogVisible = ref(false)
const DatasourceListRef = ref()
const innerDs = ref()
const loading = ref(false)
function showDs() {
listDs()
dialogVisible.value = true
datasourceConfigvVisible.value = true
}
function hideDs() {
innerDs.value = undefined
dialogVisible.value = false
datasourceConfigvVisible.value = false
}
function selectDsInDialog(ds: any) {
@@ -77,9 +98,12 @@ onMounted(() => {
if (props.hidden) {
return
}
listDs()
})
const handleAddDatasource = () => {
router.push('/ds/index')
}
defineExpose({
showDs,
hideDs,
@@ -88,79 +112,107 @@ defineExpose({
</script>
<template>
<div v-if="!hidden">
<el-drawer
ref="DatasourceListRef"
v-model="dialogVisible"
direction="btt"
:close-on-press-escape="false"
:close-on-click-modal="false"
destroy-on-close
:show-close="false"
size="100%"
>
<template #header>
<div>
<div>Choose Datasource</div>
</div>
</template>
<el-scrollbar v-loading="loading">
<div class="ds-row-container">
<template v-for="(item, _index) in dsList" :key="_index">
<DatasourceItemCard
:ds="item"
class="ds-card"
:class="[item?.id === innerDs ? 'ds-card-selected' : '']"
@click="selectDsInDialog(item)"
/>
<el-drawer
v-model="datasourceConfigvVisible"
:close-on-click-modal="false"
size="calc(100% - 100px)"
modal-class="datasource-drawer-chat"
direction="btt"
:before-close="beforeClose"
:show-close="false"
>
<template #header="{ close }">
<span style="white-space: nowrap">{{ $t('qa.select_datasource') }}</span>
<div class="flex-center" style="width: 100%">
<el-input
v-model="keywords"
clearable
style="width: 320px"
:placeholder="$t('datasource.search')"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined />
</el-icon>
</template>
</div>
</el-scrollbar>
<template #footer>
<div class="dialog-footer">
<el-button :disabled="loading" @click="hideDs">Cancel</el-button>
<el-button
type="primary"
:disabled="loading || innerDs === undefined"
@click="confirmSelectDs"
>
Confirm
</el-button>
</div>
</template>
</el-drawer>
</div>
</el-input>
</div>
<el-icon style="cursor: pointer" @click="close">
<icon_close_outlined></icon_close_outlined>
</el-icon>
</template>
<div v-if="datasourceListWithSearch.length" class="card-content">
<Card
v-for="ele in datasourceListWithSearch"
:id="ele.id"
:key="ele.id"
:name="ele.name"
:type="ele.type"
:type-name="ele.type_name"
:num="ele.num"
:is-selected="ele.id === innerDs"
:description="ele.description"
@select-ds="selectDsInDialog(ele)"
></Card>
</div>
<template v-if="!keywords && !datasourceListWithSearch.length && !searchLoading">
<EmptyBackground
class="datasource-yet_btn"
:description="$t('datasource.data_source_yet')"
img-type="noneWhite"
/>
<div style="text-align: center; margin-top: -10px">
<el-button type="primary" @click="handleAddDatasource">
<template #icon>
<icon_add_outlined></icon_add_outlined>
</template>
{{ $t('datasource.new_data_source') }}
</el-button>
</div>
</template>
<EmptyBackground
v-if="!!keywords && !datasourceListWithSearch.length"
:description="$t('datasource.relevant_content_found')"
class="datasource-yet"
img-type="tree"
/>
<template #footer>
<div class="dialog-footer">
<el-button secondary :disabled="loading" @click="hideDs">{{
$t('common.cancel')
}}</el-button>
<el-button
:type="loading || innerDs === undefined ? 'info' : 'primary'"
:disabled="loading || innerDs === undefined"
@click="confirmSelectDs"
>
{{ $t('datasource.confirm') }}
</el-button>
</div>
</template>
</el-drawer>
</template>
<style scoped lang="less">
.welcome-content {
padding: 12px;
}
<style lang="less">
.datasource-drawer-chat {
.card-content {
display: flex;
flex-wrap: wrap;
max-height: 100%;
overflow-y: auto;
}
.sub {
color: grey;
font-size: 0.8em;
}
.datasource-yet {
padding-bottom: 0;
height: auto;
padding-top: 200px;
}
.ds-select-row {
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
}
.ds-row-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.ds-card {
cursor: pointer;
}
.ds-card-selected {
box-shadow: 0 1px 3px var(--ed-color-primary-light-5);
border: 1px solid var(--ed-color-primary-light-5);
.datasource-yet_btn {
height: auto !important;
padding-top: 200px;
padding-bottom: 0;
}
}
</style>
+170
View File
@@ -0,0 +1,170 @@
<script lang="ts" setup>
import { onMounted, ref, computed, shallowRef } from 'vue'
import icon_close_outlined from '@/assets/svg/operate/ope-close.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { chatApi, ChatInfo } from '@/api/chat.ts'
import { datasourceApi } from '@/api/datasource.ts'
import Card from '@/views/ds/ChatCard.vue'
const props = withDefaults(
defineProps<{
hidden?: boolean
}>(),
{
hidden: false,
}
)
const datasourceConfigvVisible = ref(false)
const keywords = ref('')
const datasourceList = shallowRef([] as any[])
const datasourceListWithSearch = computed(() => {
if (!keywords.value) return datasourceList.value
return datasourceList.value.filter((ele) =>
ele.name.toLowerCase().includes(keywords.value.toLowerCase())
)
})
const beforeClose = () => {
datasourceConfigvVisible.value = false
keywords.value = ''
}
const emits = defineEmits(['onChatCreated'])
function listDs() {
datasourceApi.list().then((res) => {
datasourceList.value = res
})
}
const dialogVisible = ref(false)
const innerDs = ref()
const loading = ref(false)
function showDs() {
listDs()
dialogVisible.value = true
}
function hideDs() {
innerDs.value = undefined
dialogVisible.value = false
}
function selectDsInDialog(ds: any) {
innerDs.value = ds.id
}
function confirmSelectDs() {
if (innerDs.value) {
createChat(innerDs.value)
}
}
function createChat(datasource: number) {
loading.value = true
chatApi
.startChat({
datasource: datasource,
})
.then((res) => {
const chat: ChatInfo | undefined = chatApi.toChatInfo(res)
if (chat == undefined) {
throw Error('chat is undefined')
}
emits('onChatCreated', chat)
hideDs()
})
.catch((e) => {
console.error(e)
})
.finally(() => {
loading.value = false
})
}
onMounted(() => {
if (props.hidden) {
return
}
})
defineExpose({
showDs,
hideDs,
createChat,
})
</script>
<template>
<el-drawer
v-model="datasourceConfigvVisible"
:close-on-click-modal="false"
size="calc(100% - 100px)"
modal-class="datasource-drawer-chat"
direction="btt"
:before-close="beforeClose"
:show-close="false"
>
<template #header="{ close }">
<span style="white-space: nowrap">{{ $t('qa.select_datasource') }}</span>
<div class="flex-center" style="width: 100%">
<el-input
v-model="keywords"
clearable
style="width: 320px"
:placeholder="$t('datasource.search')"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined />
</el-icon>
</template>
</el-input>
</div>
<el-icon style="cursor: pointer" @click="close">
<icon_close_outlined></icon_close_outlined>
</el-icon>
</template>
<div class="card-content">
<Card
v-for="ele in datasourceListWithSearch"
:id="ele.id"
:key="ele.id"
:name="ele.name"
:type="ele.type"
:type-name="ele.type_name"
:num="ele.num"
:description="ele.description"
@select-ds="selectDsInDialog(ele)"
></Card>
</div>
<template #footer>
<div class="dialog-footer">
<el-button secondary :disabled="loading" @click="hideDs">{{
$t('common.cancel')
}}</el-button>
<el-button
type="primary"
:disabled="loading || innerDs === undefined"
@click="confirmSelectDs"
>
{{ $t('datasource.confirm') }}
</el-button>
</div>
</template>
</el-drawer>
</template>
<style lang="less">
.datasource-drawer-chat {
.card-content {
display: flex;
flex-wrap: wrap;
max-height: calc(100% - 40px);
overflow-y: auto;
}
}
</style>
+9 -6
View File
@@ -412,7 +412,7 @@ const createNewChat = async () => {
chatCreatorRef.value?.showDs()
}
function getChatList() {
function getChatList(callback?: () => void) {
loading.value = true
chatApi
.list()
@@ -421,6 +421,9 @@ function getChatList() {
})
.finally(() => {
loading.value = false
if (callback && typeof callback === 'function') {
callback()
}
})
}
@@ -754,10 +757,6 @@ function stop(func?: (...p: any[]) => void, ...param: any[]) {
}
}
onMounted(() => {
getChatList()
})
defineExpose({
getHistoryList,
toAssistantHistory,
@@ -767,7 +766,7 @@ defineExpose({
const hiddenChatCreatorRef = ref()
onMounted(() => {
function jumpCreatChat() {
if (props.startChatDsId) {
const _id = props.startChatDsId
nextTick(() => {
@@ -776,6 +775,10 @@ onMounted(() => {
const newUrl = window.location.hash.replace(/\?.*$/, '')
history.replaceState({}, '', newUrl)
}
}
onMounted(() => {
getChatList(jumpCreatChat)
})
</script>
+169
View File
@@ -0,0 +1,169 @@
<script lang="ts" setup>
import icon_form_outlined from '@/assets/svg/icon_form_outlined.svg'
import { computed } from 'vue'
import { dsTypeWithImg } from './js/ds-type'
const props = withDefaults(
defineProps<{
name: string
type: string
typeName: string
num: string
isSelected: boolean
description?: string
id?: string
}>(),
{
name: '-',
type: '-',
description: '-',
id: '-',
typeName: '-',
isSelected: false,
}
)
const emits = defineEmits(['selectDs'])
const icon = computed(() => {
return (dsTypeWithImg.find((ele) => props.type === ele.type) || {}).img
})
const SelectDs = () => {
emits('selectDs')
}
</script>
<template>
<div class="card" @click="SelectDs" :class="isSelected && 'is-selected'">
<div class="name-icon">
<img :src="icon" width="32px" height="32px" />
<div class="info">
<div :title="name" class="name ellipsis">{{ name }}</div>
<div class="type">{{ typeName }}</div>
</div>
</div>
<div :title="description" class="type-value">
{{ description }}
</div>
<div class="bottom-info">
<div class="form-rate">
<el-icon class="form-icon" size="16">
<icon_form_outlined></icon_form_outlined>
</el-icon>
{{ num }}
</div>
<div click.stop class="methods"></div>
</div>
</div>
</template>
<style lang="less" scoped>
.card {
width: 370px;
border: 1px solid #dee0e3;
padding: 16px;
border-radius: 12px;
cursor: pointer;
margin: 0 16px 16px 0;
&:hover {
box-shadow: 0px 6px 24px 0px #1f232914;
}
.name-icon {
display: flex;
align-items: center;
margin-bottom: 12px;
.info {
margin-left: 12px;
.name {
font-weight: 500;
font-size: 16px;
line-height: 24px;
max-width: 250px;
}
.type {
font-weight: 400;
font-size: 12px;
line-height: 20px;
color: #646a73;
}
}
}
.type-value {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
height: 44px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
width: 100%;
}
.bottom-info {
margin-top: 16px;
display: flex;
align-items: center;
justify-content: space-between;
height: 28px;
.ed-button {
height: 28px;
min-width: 78px;
}
.form-rate {
display: flex;
align-items: center;
color: #646a73;
font-weight: 400;
font-size: 14px;
line-height: 22px;
.form-icon {
margin-right: 8px;
}
}
.methods {
align-items: center;
display: none;
.more {
position: relative;
cursor: pointer;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
}
&.is-selected {
border: 1px solid #1cba90;
background: #1cba901a;
}
}
</style>
+24 -14
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref, onMounted, reactive } from 'vue'
import { ref, onMounted, reactive, nextTick } from 'vue'
import {
uwsOption,
workspaceUserList,
@@ -24,7 +24,7 @@ const workspaceForm = reactive({
name: '',
id: '',
})
const selectable = (row: any) => ![userStore.getUid].includes(row.id)
const selectable = (row: any) => ![userStore.getUid].includes(row.id) && row.weight !== 1
onMounted(() => {
search()
@@ -90,17 +90,19 @@ const deleteHandler = (row: any) => {
})
}
const handleSelectionChange = (val: any[]) => {
const ids = fieldList.value.map((ele: any) => ele.id)
if (toggleRowLoading.value) return
const arr = fieldList.value.filter(selectable)
const ids = arr.map((ele: any) => ele.id)
multipleSelectionAll.value = [
...multipleSelectionAll.value.filter((ele) => !ids.includes(ele.id)),
...val,
]
isIndeterminate.value = !(val.length === 0 || val.length === fieldList.value.length)
checkAll.value = val.length === fieldList.value.length
isIndeterminate.value = !(val.length === 0 || val.length === arr.length)
checkAll.value = val.length === arr.length
}
const handleCheckAllChange = (val: any) => {
isIndeterminate.value = false
handleSelectionChange(val ? fieldList.value : [])
handleSelectionChange(val ? fieldList.value.filter(selectable) : [])
if (val) {
handleToggleRowSelection()
} else {
@@ -108,17 +110,22 @@ const handleCheckAllChange = (val: any) => {
}
}
const toggleRowLoading = ref(false)
const handleToggleRowSelection = (check: boolean = true) => {
toggleRowLoading.value = true
const arr = fieldList.value.filter(selectable)
let i = 0
const ids = multipleSelectionAll.value.map((ele: any) => ele.id)
for (const key in fieldList.value) {
if (ids.includes((fieldList.value[key] as any).id)) {
for (const key in arr) {
if (ids.includes((arr[key] as any).id)) {
i += 1
multipleTableRef.value.toggleRowSelection(fieldList.value[key], check)
multipleTableRef.value.toggleRowSelection(arr[key], check)
}
}
checkAll.value = i === fieldList.value.length
isIndeterminate.value = !(i === 0 || i === fieldList.value.length)
toggleRowLoading.value = false
checkAll.value = i === arr.length
isIndeterminate.value = !(i === 0 || i === arr.length)
}
const search = () => {
@@ -127,8 +134,12 @@ const search = () => {
pageInfo.currentPage,
pageInfo.pageSize
).then((res) => {
toggleRowLoading.value = true
fieldList.value = res.items
pageInfo.total = res.total
nextTick(() => {
handleToggleRowSelection()
})
})
}
@@ -220,11 +231,10 @@ const handleCurrentChange = (val: number) => {
<el-table
ref="multipleTableRef"
:data="fieldList"
:selectable="selectable"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column :selectable="selectable" type="selection" width="55" />
<el-table-column prop="name" :label="$t('user.name')" width="160" />
<el-table-column prop="account" :label="$t('user.account')" width="160" />
<el-table-column prop="status" :label="$t('user.user_status')">
@@ -288,7 +298,7 @@ const handleCurrentChange = (val: number) => {
</el-table>
</div>
</div>
<div class="pagination-container">
<div v-if="fieldList.length" class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.currentPage"
v-model:page-size="pageInfo.pageSize"
@@ -216,9 +216,13 @@ defineExpose({
.flex {
display: flex;
}
.ed-checkbox {
margin-right: 0 !important;
}
.border-r {
border-right: 1px solid #dee0e3;
width: 50%;
}
}
</style>
@@ -38,7 +38,7 @@ const props = withDefaults(defineProps<Props>(), {
const { t } = useI18n()
const showDel = ref(false)
const keywords = ref('')
const activeName = ref({})
const activeName = ref()
const checklist = ref<string[]>([])
const filterList = ref<any[]>([])
@@ -152,7 +152,22 @@ const emits = defineEmits(['update:item', 'del'])
>
</el-option>
</el-select>
<el-select
v-if="['null', 'empty'].includes(item.term)"
v-model="item.term"
style="max-width: 280px; margin-left: 8px"
:placeholder="t('datasource.Please_select')"
>
<el-option
v-for="ele in operators"
:key="ele.value"
:label="t(ele.label)"
:value="ele.value"
>
</el-option>
</el-select>
<el-input
v-else
v-model="item.value"
style="max-width: 280px; margin-left: 8px"
:placeholder="$t('datasource.please_enter')"
+57 -15
View File
@@ -25,8 +25,8 @@ const termFormRef = ref()
const columnFormRef = ref()
const drawerTitle = ref('')
const dialogTitle = ref('')
const activeDs = ref({})
const activeTable = ref({})
const activeDs = ref(null)
const activeTable = ref(null)
const ruleList = ref<any[]>([])
const defaultPermission = {
@@ -146,8 +146,8 @@ const saveAuthTree = (val: any) => {
dialogFormVisible.value = false
}
const getDsList = (row: any) => {
activeDs.value = {}
activeTable.value = {}
activeDs.value = null
activeTable.value = null
datasourceApi
.list()
.then((res: any) => {
@@ -211,6 +211,17 @@ const handleColumnPermission = (row: any) => {
: t('permission.add_column_permission')
}
const handleInitDsIdChange = (val: any) => {
columnForm.ds_id = val.id
columnForm.ds_name = val.name
datasourceApi.tableList(val.id).then((res: any) => {
tableListOptions.value = res || []
activeTable.value = null
fieldListOptions.value = []
columnForm.permissions = []
})
}
const handleDsIdChange = (val: any) => {
columnForm.ds_id = val.id
columnForm.ds_name = val.name
@@ -259,6 +270,9 @@ const handleEditeTable = (val: any) => {
})
}
const beforeClose = () => {
if (termFormRef.value) {
termFormRef.value.clearValidate()
}
ruleConfigvVisible.value = false
activeStep.value = 0
isCreate.value = false
@@ -398,8 +412,8 @@ const saveHandler = () => {
})
)
}
dialogFormVisible.value = false
}
dialogFormVisible.value = false
}
})
}
@@ -417,7 +431,8 @@ const next = () => {
}
})
}
const savePermission = () => {
const save = () => {
const { id, name, permissions, users } = cloneDeep(currentPermission)
const permissionsObj = permissions.map((ele: any) => {
@@ -459,6 +474,18 @@ const savePermission = () => {
handleSearch()
})
}
const savePermission = () => {
if (!isCreate.value && activeStep.value === 0) {
termFormRef.value.validate((res: any) => {
if (res) {
save()
}
})
return
}
save()
}
const columnRules = {
name: [
@@ -471,8 +498,8 @@ const columnRules = {
table_id: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('permission.rule_group_name'),
trigger: 'blur',
message: t('datasource.Please_select') + t('common.empty') + t('permission.set_rule'),
trigger: 'change',
},
],
}
@@ -485,10 +512,9 @@ const columnRules = {
<div>
<el-input
v-model="keywords"
clearable
style="width: 240px; margin-right: 12px"
:placeholder="$t('permission.search_rule_group')"
@keyup.enter="handleSearch"
@blur="handleSearch"
>
<template #prefix>
<el-icon>
@@ -526,7 +552,11 @@ const columnRules = {
></Card>
</div>
<template v-if="!keywords && !ruleListWithSearch.length && !searchLoading">
<EmptyBackground :description="$t('permission.no_permission_rule')" img-type="noneWhite" />
<EmptyBackground
class="ed-empty_200"
:description="$t('permission.no_permission_rule')"
img-type="noneWhite"
/>
<div style="text-align: center; margin-top: -10px">
<el-button type="primary" @click="addHandler()">
@@ -579,6 +609,8 @@ const columnRules = {
>
<el-form-item prop="name" :label="t('permission.rule_group_name')">
<el-input
maxlength="50"
clearable
v-model="currentPermission.name"
:placeholder="
$t('datasource.please_enter') +
@@ -722,6 +754,8 @@ const columnRules = {
<el-form-item prop="name" :label="t('permission.rule_name')">
<el-input
v-model="columnForm.name"
maxlength="50"
clearable
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('permission.rule_name')
"
@@ -731,12 +765,13 @@ const columnRules = {
<el-form-item prop="table_id" :label="t('permission.data_table')">
<el-select
v-model="activeDs"
filterable
style="width: 416px"
value-key="id"
:placeholder="
$t('datasource.Please_select') + $t('common.empty') + $t('permission.data_source')
"
@change="handleDsIdChange"
@change="handleInitDsIdChange"
>
<el-option
v-for="item in dsListOptions"
@@ -747,6 +782,7 @@ const columnRules = {
</el-select>
<el-select
v-model="activeTable"
filterable
style="width: 416px; margin-left: auto"
:disabled="!columnForm.ds_id"
value-key="id"
@@ -809,7 +845,9 @@ const columnRules = {
<style lang="less" scoped>
.permission {
.ed-empty {
height: 100%;
width: 100%;
.ed-empty_200 {
padding-top: 200px;
padding-bottom: 0;
}
@@ -864,6 +902,10 @@ const columnRules = {
padding-right: 0;
}
.ed-form-item__content {
padding-bottom: 100px;
}
.table-content {
width: 100%;
margin-top: 16px;
@@ -871,7 +913,7 @@ const columnRules = {
border-top: none;
border-bottom: none;
border-radius: 6px;
overflow: hidden;
overflow-y: auto;
max-height: calc(100vh - 400px);
&.border-bottom {
@@ -929,7 +971,7 @@ const columnRules = {
border: 1px solid #1f232926;
border-top: none;
border-radius: 6px;
overflow: hidden;
overflow-y: auto;
max-height: calc(100vh - 400px);
.ed-table__empty-text {
padding-top: 0;
+1 -1
View File
@@ -137,7 +137,7 @@
</template>
</el-table>
</div>
<div class="pagination-container">
<div v-if="state.tableData.length" class="pagination-container">
<el-pagination
v-model:current-page="state.pageInfo.currentPage"
v-model:page-size="state.pageInfo.pageSize"
@@ -480,7 +480,7 @@ const handleCurrentChange = (val: number) => {
</el-table>
</div>
</div>
<div class="pagination-container">
<div v-if="fieldListWithSearch.length" class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.currentPage"
v-model:page-size="pageInfo.pageSize"
@@ -602,6 +602,7 @@ const handleCurrentChange = (val: number) => {
.list-content {
height: calc(100% - 100px);
overflow-y: auto;
.more {
display: none;