[Core] Support some fc (#732)

This commit is contained in:
qianmoQ 2024-04-11 10:49:33 +08:00 committed by GitHub
commit a92dd877f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
279 changed files with 1686 additions and 10189 deletions

View File

@ -53,6 +53,12 @@ public class SourceV2Controller
return this.sourceService.getByIdV2(id);
}
@GetMapping(value = "code/{code}")
public CommonResponse<SourceEntity> getByCode(@PathVariable(value = "code") String code)
{
return this.sourceService.getByCode(code);
}
@PostMapping(value = "getHistory/{id}")
public CommonResponse<PageEntity<ScheduledHistoryEntity>> getHistory(@PathVariable(value = "id") Long id,
@RequestBody FilterBody filter)

View File

@ -16,3 +16,10 @@ WHERE `id` = 16;
UPDATE `menus`
SET `url` = '/admin/query'
WHERE `id` = 2;
ALTER TABLE `datacap_source`
ADD COLUMN `code` VARCHAR(100);
UPDATE `datacap_source`
SET `code` = REPLACE(UUID(), '-', '')
WHERE `code` IS NULL;

View File

@ -122,6 +122,9 @@ public class SourceEntity
@Column(name = "message")
private String message;
@Column(name = "code")
private String code;
@Column(name = "create_time")
@CreatedDate
private Timestamp createTime;

View File

@ -6,5 +6,6 @@ public enum QueryMode
HISTORY,
REPORT,
SNIPPET,
SYNC
SYNC,
DATASET
}

View File

@ -7,6 +7,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.PagingAndSortingRepository;
import java.util.List;
import java.util.Optional;
public interface SourceRepository
extends PagingAndSortingRepository<SourceEntity, Long>
@ -15,6 +16,8 @@ public interface SourceRepository
SourceEntity findByName(String name);
Optional<SourceEntity> findByCode(String code);
Page<SourceEntity> findAllByUserOrPublishIsTrue(UserEntity user, Pageable pageable);
Long countByUserOrPublishIsTrue(UserEntity user);

View File

@ -26,6 +26,8 @@ public interface SourceService
CommonResponse<SourceEntity> getById(Long id);
CommonResponse<SourceEntity> getByCode(String code);
CommonResponse<Map<String, List<PluginEntity>>> getPlugins();
CommonResponse<Long> count();

View File

@ -35,4 +35,11 @@ public class ChatServiceImpl
Pageable pageable = PageRequestAdapter.of(filter);
return CommonResponse.success(PageEntity.build(repository.findAllByUser(UserDetailsService.getUser(), pageable)));
}
@Override
public CommonResponse<ChatEntity> saveOrUpdate(PagingAndSortingRepository repository, ChatEntity configure)
{
configure.setUser(UserDetailsService.getUser());
return CommonResponse.success(repository.save(configure));
}
}

View File

@ -2,7 +2,6 @@ package io.edurt.datacap.service.service.impl;
import com.google.common.collect.Lists;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.edurt.datacap.common.enums.ServiceState;
import io.edurt.datacap.common.response.CommonResponse;
import io.edurt.datacap.common.utils.AiSupportUtils;
import io.edurt.datacap.common.utils.JsonUtils;
@ -34,7 +33,6 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -60,16 +58,12 @@ public class MessageServiceImpl
@Override
public CommonResponse<MessageEntity> saveOrUpdate(PagingAndSortingRepository repository, MessageEntity configure)
{
Optional<UserEntity> userOptional = this.userRepository.findById(configure.getUser().getId());
if (!userOptional.isPresent()) {
return CommonResponse.failure(ServiceState.USER_NOT_FOUND);
}
String openApiHost = environment.getProperty("datacap.openai.backend");
String openApiToken = environment.getProperty("datacap.openai.token");
String openApiModel = environment.getProperty("datacap.openai.model");
long openApiTimeout = Long.parseLong(environment.getProperty("datacap.openai.timeout"));
UserEntity user = userOptional.get();
UserEntity user = UserDetailsService.getUser();
MessageEntity questionMessage = MessageEntity.builder()
.user(user)
.chat(configure.getChat())

View File

@ -14,12 +14,15 @@ import io.edurt.datacap.executor.common.RunWay;
import io.edurt.datacap.executor.configure.ExecutorConfigure;
import io.edurt.datacap.executor.configure.ExecutorRequest;
import io.edurt.datacap.executor.configure.ExecutorResponse;
import io.edurt.datacap.service.adapter.PageRequestAdapter;
import io.edurt.datacap.service.body.FilterBody;
import io.edurt.datacap.service.body.PipelineBody;
import io.edurt.datacap.service.common.ConfigureUtils;
import io.edurt.datacap.service.common.FolderUtils;
import io.edurt.datacap.service.configure.FieldType;
import io.edurt.datacap.service.configure.IConfigureExecutorField;
import io.edurt.datacap.service.configure.IConfigurePipelineType;
import io.edurt.datacap.service.entity.PageEntity;
import io.edurt.datacap.service.entity.PipelineEntity;
import io.edurt.datacap.service.entity.SourceEntity;
import io.edurt.datacap.service.initializer.InitializerConfigure;
@ -68,6 +71,12 @@ public class PipelineServiceImpl
this.initializer = initializer;
}
@Override
public CommonResponse<PageEntity> getAll(PagingAndSortingRepository repository1, FilterBody filter)
{
return CommonResponse.success(repository.findAllByUser(UserDetailsService.getUser(), PageRequestAdapter.of(filter)));
}
@Override
public CommonResponse<Object> submit(PipelineBody configure)
{

View File

@ -65,6 +65,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
@ -107,6 +108,7 @@ public class SourceServiceImpl
{
configure.setConfigure(JsonUtils.toJSON(configure.getConfigures()));
configure.setUser(UserDetailsService.getUser());
configure.setCode(UUID.randomUUID().toString().replace("-", ""));
return CommonResponse.success(this.sourceRepository.save(configure));
}
@ -117,14 +119,12 @@ public class SourceServiceImpl
UserEntity user = UserDetailsService.getUser();
Page<SourceEntity> page = this.sourceRepository.findAllByUserOrPublishIsTrue(user, pageable);
// Populate pipeline configuration information
page.getContent()
.stream()
.forEach(item -> {
IConfigure fromConfigure = PluginUtils.loadYamlConfigure(item.getProtocol(), item.getType(), item.getType(), environment);
if (fromConfigure != null) {
item.setPipelines(fromConfigure.getPipelines());
}
});
page.getContent().stream().forEach(item -> {
IConfigure fromConfigure = PluginUtils.loadYamlConfigure(item.getProtocol(), item.getType(), item.getType(), environment);
if (fromConfigure != null) {
item.setPipelines(fromConfigure.getPipelines());
}
});
return CommonResponse.success(PageEntity.build(page));
}
@ -183,6 +183,14 @@ public class SourceServiceImpl
return CommonResponse.success(this.sourceRepository.findById(id));
}
@Override
public CommonResponse<SourceEntity> getByCode(String code)
{
return this.sourceRepository.findByCode(code)
.map(item -> CommonResponse.success(item))
.orElseGet(() -> CommonResponse.failure(String.format("Source [ %s ] not found", code)));
}
@Override
public CommonResponse<Map<String, List<PluginEntity>>> getPlugins()
{
@ -361,22 +369,17 @@ public class SourceServiceImpl
public CommonResponse<PageEntity<ScheduledHistoryEntity>> getHistory(Long id, FilterBody filter)
{
Pageable pageable = PageRequestAdapter.of(filter);
SourceEntity entity = SourceEntity.builder()
.id(id)
.build();
SourceEntity entity = SourceEntity.builder().id(id).build();
return CommonResponse.success(PageEntity.build(this.scheduledHistoryRepository.findAllBySource(entity, pageable)));
}
@Override
public CommonResponse<SourceEntity> syncMetadata(Long id)
{
return this.sourceRepository.findById(id)
.map(entity -> {
Executors.newSingleThreadExecutor()
.submit(() -> startSyncMetadata(entity, null));
return CommonResponse.success(entity);
})
.orElseGet(() -> CommonResponse.failure(String.format("Source [ %s ] not found", id)));
return this.sourceRepository.findById(id).map(entity -> {
Executors.newSingleThreadExecutor().submit(() -> startSyncMetadata(entity, null));
return CommonResponse.success(entity);
}).orElseGet(() -> CommonResponse.failure(String.format("Source [ %s ] not found", id)));
}
private void startSyncMetadata(SourceEntity entity, ScheduledEntity scheduled)
@ -394,12 +397,7 @@ public class SourceServiceImpl
Map<String, List<TableEntity>> databaseTableCache = Maps.newHashMap();
Map<String, TableEntity> tableCache = Maps.newHashMap();
Map<String, List<ColumnEntity>> tableColumnCache = Maps.newHashMap();
ScheduledHistoryEntity scheduledHistory = ScheduledHistoryEntity.builder()
.name(String.format("Sync source [ %s ]", entity.getName()))
.scheduled(scheduled)
.source(entity)
.state(RunState.RUNNING)
.build();
ScheduledHistoryEntity scheduledHistory = ScheduledHistoryEntity.builder().name(String.format("Sync source [ %s ]", entity.getName())).scheduled(scheduled).source(entity).state(RunState.RUNNING).build();
scheduledHistoryHandler.save(scheduledHistory);
log.info("==================== Sync metadata [ {} ] started =================", entity.getName());
Optional<Plugin> pluginOptional = PluginUtils.getPluginByNameAndType(this.injector, entity.getType(), entity.getProtocol());
@ -415,21 +413,7 @@ public class SourceServiceImpl
log.error("The source [ {} ] not available", entity.getName());
}
else {
this.startSyncDatabase(entity,
plugin,
databaseCache,
databaseTableCache,
tableCache,
tableColumnCache,
databaseAddedCount,
databaseUpdatedCount,
databaseRemovedCount,
tableAddedCount,
tableUpdatedCount,
tableRemovedCount,
columnAddedCount,
columnUpdatedCount,
columnRemovedCount);
this.startSyncDatabase(entity, plugin, databaseCache, databaseTableCache, tableCache, tableColumnCache, databaseAddedCount, databaseUpdatedCount, databaseRemovedCount, tableAddedCount, tableUpdatedCount, tableRemovedCount, columnAddedCount, columnUpdatedCount, columnRemovedCount);
}
scheduledHistory.setState(RunState.SUCCESS);
}
@ -471,22 +455,18 @@ public class SourceServiceImpl
if (ObjectUtils.isNotEmpty(entity.getConfigure())) {
final String[] content = {entity.getContent()};
List<LinkedHashMap> configures = JsonUtils.objectmapper.readValue(entity.getConfigure(), List.class);
map.entrySet()
.forEach(value -> {
Optional<SqlConfigure> sqlConfigure = configures.stream()
.filter(v -> String.valueOf(v.get("column")).equalsIgnoreCase(value.getKey()))
.map(v -> {
SqlConfigure configure = new SqlConfigure();
configure.setColumn(String.valueOf(v.get("column")));
configure.setType(Type.valueOf(String.valueOf(v.get("type"))));
configure.setExpression(String.valueOf(v.get("expression")));
return configure;
})
.findFirst();
if (sqlConfigure.isPresent()) {
content[0] = content[0].replace(sqlConfigure.get().getExpression(), String.valueOf(value.getValue()));
}
});
map.entrySet().forEach(value -> {
Optional<SqlConfigure> sqlConfigure = configures.stream().filter(v -> String.valueOf(v.get("column")).equalsIgnoreCase(value.getKey())).map(v -> {
SqlConfigure configure = new SqlConfigure();
configure.setColumn(String.valueOf(v.get("column")));
configure.setType(Type.valueOf(String.valueOf(v.get("type"))));
configure.setExpression(String.valueOf(v.get("expression")));
return configure;
}).findFirst();
if (sqlConfigure.isPresent()) {
content[0] = content[0].replace(sqlConfigure.get().getExpression(), String.valueOf(value.getValue()));
}
});
return content[0];
}
}
@ -547,21 +527,7 @@ public class SourceServiceImpl
* @param columnUpdatedCount the AtomicInteger object representing the count of updated columns
* @param columnRemovedCount the AtomicInteger object representing the count of removed columns
*/
private void startSyncDatabase(SourceEntity entity,
Plugin plugin,
Map<String, DatabaseEntity> databaseCache,
Map<String, List<TableEntity>> databaseTableCache,
Map<String, TableEntity> tableCache,
Map<String, List<ColumnEntity>> tableColumnCache,
AtomicInteger databaseAddedCount,
AtomicInteger databaseUpdatedCount,
AtomicInteger databaseRemovedCount,
AtomicInteger tableAddedCount,
AtomicInteger tableUpdatedCount,
AtomicInteger tableRemovedCount,
AtomicInteger columnAddedCount,
AtomicInteger columnUpdatedCount,
AtomicInteger columnRemovedCount)
private void startSyncDatabase(SourceEntity entity, Plugin plugin, Map<String, DatabaseEntity> databaseCache, Map<String, List<TableEntity>> databaseTableCache, Map<String, TableEntity> tableCache, Map<String, List<ColumnEntity>> tableColumnCache, AtomicInteger databaseAddedCount, AtomicInteger databaseUpdatedCount, AtomicInteger databaseRemovedCount, AtomicInteger tableAddedCount, AtomicInteger tableUpdatedCount, AtomicInteger tableRemovedCount, AtomicInteger columnAddedCount, AtomicInteger columnUpdatedCount, AtomicInteger columnRemovedCount)
{
String templateName = "SYSTEM_FOR_GET_ALL_DATABASES";
TemplateSqlEntity template = getTemplate(templateName, entity);
@ -576,29 +542,19 @@ public class SourceServiceImpl
}
else {
List<DatabaseEntity> origin = databaseHandler.findAllBySource(entity);
List<DatabaseEntity> entities = response.getColumns()
.stream()
.map(item -> {
DatabaseEntity database = DatabaseEntity.builder()
.name(getNodeText(item, NodeType.SCHEMA))
.catalog(getNodeText(item, NodeType.CATALOG))
.description(String.format("[ %s ] of [ %s ]", getNodeText(item, NodeType.SCHEMA), getNodeText(item, NodeType.CATALOG)))
.source(entity)
.build();
Optional<DatabaseEntity> optionalDatabase = origin.stream()
.filter(node -> node.getName().equals(database.getName()))
.findAny();
if (optionalDatabase.isPresent()) {
database.setId(optionalDatabase.get().getId());
database.setCreateTime(optionalDatabase.get().getCreateTime());
databaseUpdatedCount.addAndGet(1);
}
else {
databaseAddedCount.addAndGet(1);
}
return database;
})
.collect(Collectors.toList());
List<DatabaseEntity> entities = response.getColumns().stream().map(item -> {
DatabaseEntity database = DatabaseEntity.builder().name(getNodeText(item, NodeType.SCHEMA)).catalog(getNodeText(item, NodeType.CATALOG)).description(String.format("[ %s ] of [ %s ]", getNodeText(item, NodeType.SCHEMA), getNodeText(item, NodeType.CATALOG))).source(entity).build();
Optional<DatabaseEntity> optionalDatabase = origin.stream().filter(node -> node.getName().equals(database.getName())).findAny();
if (optionalDatabase.isPresent()) {
database.setId(optionalDatabase.get().getId());
database.setCreateTime(optionalDatabase.get().getCreateTime());
databaseUpdatedCount.addAndGet(1);
}
else {
databaseAddedCount.addAndGet(1);
}
return database;
}).collect(Collectors.toList());
// Write the new data retrieved to the database
log.info("Added database size [ {} ] to source [ {} ]", entities.size(), entity.getName());
databaseHandler.saveAll(entities);
@ -608,25 +564,12 @@ public class SourceServiceImpl
databaseTableCache.put(key, this.tableHandler.findSimpleAllByDatabase(item));
});
// Delete invalid data that no longer exists
List<DatabaseEntity> deleteEntities = origin.stream()
.filter(node -> entities.stream().noneMatch(item -> node.getName().equals(item.getName())))
.collect(Collectors.toList());
List<DatabaseEntity> deleteEntities = origin.stream().filter(node -> entities.stream().noneMatch(item -> node.getName().equals(item.getName()))).collect(Collectors.toList());
log.info("Removed database size [ {} ] from source [ {} ]", deleteEntities.size(), entity.getName());
databaseHandler.deleteAll(deleteEntities);
databaseRemovedCount.addAndGet(deleteEntities.size());
}
this.startSyncTable(entity,
plugin,
databaseCache,
databaseTableCache,
tableCache,
tableColumnCache,
tableAddedCount,
tableUpdatedCount,
tableRemovedCount,
columnAddedCount,
columnUpdatedCount,
columnRemovedCount);
this.startSyncTable(entity, plugin, databaseCache, databaseTableCache, tableCache, tableColumnCache, tableAddedCount, tableUpdatedCount, tableRemovedCount, columnAddedCount, columnUpdatedCount, columnRemovedCount);
}
}
@ -646,18 +589,7 @@ public class SourceServiceImpl
* @param columnUpdatedCount the column updated count
* @param columnRemovedCount the column removed count
*/
private void startSyncTable(SourceEntity entity,
Plugin plugin,
Map<String, DatabaseEntity> databaseCache,
Map<String, List<TableEntity>> databaseTableCache,
Map<String, TableEntity> tableCache,
Map<String, List<ColumnEntity>> tableColumnCache,
AtomicInteger tableAddedCount,
AtomicInteger tableUpdatedCount,
AtomicInteger tableRemovedCount,
AtomicInteger columnAddedCount,
AtomicInteger columnUpdatedCount,
AtomicInteger columnRemovedCount)
private void startSyncTable(SourceEntity entity, Plugin plugin, Map<String, DatabaseEntity> databaseCache, Map<String, List<TableEntity>> databaseTableCache, Map<String, TableEntity> tableCache, Map<String, List<ColumnEntity>> tableColumnCache, AtomicInteger tableAddedCount, AtomicInteger tableUpdatedCount, AtomicInteger tableRemovedCount, AtomicInteger columnAddedCount, AtomicInteger columnUpdatedCount, AtomicInteger columnRemovedCount)
{
String templateName = "SYSTEM_FOR_GET_ALL_TABLES";
TemplateSqlEntity template = getTemplate(templateName, entity);
@ -671,43 +603,20 @@ public class SourceServiceImpl
log.error("The source [ {} ] protocol [ {} ] sync metadata tables [ {} ] failed", entity.getName(), entity.getProtocol(), response.getMessage());
}
else {
List<TableEntity> entities = response.getColumns()
.stream()
.map(item -> {
String key = String.format("%s_%s", getNodeText(item, NodeType.CATALOG), getNodeText(item, NodeType.SCHEMA));
DatabaseEntity database = databaseCache.get(key);
String name = getNodeText(item, NodeType.TABLE);
return TableEntity.builder()
.name(name)
.description(String.format("Table [ %s ] of database [ %s ] ", name, getNodeText(item, NodeType.SCHEMA)))
.type(getNodeText(item, NodeType.TYPE))
.engine(getNodeText(item, NodeType.ENGINE))
.format(getNodeText(item, NodeType.FORMAT))
.inCreateTime(getNodeText(item, NodeType.CREATE_TIME))
.inUpdateTime(getNodeText(item, NodeType.UPDATE_TIME))
.collation(getNodeText(item, NodeType.COLLATION))
.rows(getNodeText(item, NodeType.ROWS))
.comment(getNodeText(item, NodeType.COMMENT))
.avgRowLength(getNodeText(item, NodeType.AVG_ROW))
.dataLength(getNodeText(item, NodeType.DATA))
.indexLength(getNodeText(item, NodeType.INDEX))
.autoIncrement(getNodeText(item, NodeType.AUTO_INCREMENT))
.database(database)
.build();
})
.collect(Collectors.toList());
List<TableEntity> entities = response.getColumns().stream().map(item -> {
String key = String.format("%s_%s", getNodeText(item, NodeType.CATALOG), getNodeText(item, NodeType.SCHEMA));
DatabaseEntity database = databaseCache.get(key);
String name = getNodeText(item, NodeType.TABLE);
return TableEntity.builder().name(name).description(String.format("Table [ %s ] of database [ %s ] ", name, getNodeText(item, NodeType.SCHEMA))).type(getNodeText(item, NodeType.TYPE)).engine(getNodeText(item, NodeType.ENGINE)).format(getNodeText(item, NodeType.FORMAT)).inCreateTime(getNodeText(item, NodeType.CREATE_TIME)).inUpdateTime(getNodeText(item, NodeType.UPDATE_TIME)).collation(getNodeText(item, NodeType.COLLATION)).rows(getNodeText(item, NodeType.ROWS)).comment(getNodeText(item, NodeType.COMMENT)).avgRowLength(getNodeText(item, NodeType.AVG_ROW)).dataLength(getNodeText(item, NodeType.DATA)).indexLength(getNodeText(item, NodeType.INDEX)).autoIncrement(getNodeText(item, NodeType.AUTO_INCREMENT)).database(database).build();
}).collect(Collectors.toList());
Map<String, List<TableEntity>> groupEntities = entities
.stream()
.collect(Collectors.groupingBy(item -> String.format("%s_%s", item.getDatabase().getCatalog(), item.getDatabase().getName())));
Map<String, List<TableEntity>> groupEntities = entities.stream().collect(Collectors.groupingBy(item -> String.format("%s_%s", item.getDatabase().getCatalog(), item.getDatabase().getName())));
groupEntities.forEach((key, groupItem) -> {
// Detect data that needs to be updated
List<TableEntity> origin = databaseTableCache.get(key);
groupItem.forEach(item -> {
Optional<TableEntity> optionalTable = origin.stream()
.filter(node -> node.getName().equals(item.getName()))
.findAny();
Optional<TableEntity> optionalTable = origin.stream().filter(node -> node.getName().equals(item.getName())).findAny();
if (optionalTable.isPresent()) {
TableEntity node = optionalTable.get();
item.setId(node.getId());
@ -727,9 +636,7 @@ public class SourceServiceImpl
tableColumnCache.put(tableCacheKey, this.columnHandler.findSimpleAllByTable(item));
});
List<TableEntity> deleteEntities = origin.stream()
.filter(node -> groupItem.stream().noneMatch(item -> node.getName().equals(item.getName())))
.collect(Collectors.toList());
List<TableEntity> deleteEntities = origin.stream().filter(node -> groupItem.stream().noneMatch(item -> node.getName().equals(item.getName()))).collect(Collectors.toList());
log.info("Removed table size [ {} ] from database [ {} ]", deleteEntities.size(), key);
tableHandler.deleteAll(deleteEntities);
tableRemovedCount.addAndGet(deleteEntities.size());
@ -751,13 +658,7 @@ public class SourceServiceImpl
* @param columnUpdatedCount an atomic counter for tracking the number of columns updated
* @param columnRemovedCount an atomic counter for tracking the number of columns removed
*/
private void startSyncColumn(SourceEntity entity,
Plugin plugin,
Map<String, TableEntity> tableCache,
Map<String, List<ColumnEntity>> tableColumnCache,
AtomicInteger columnAddedCount,
AtomicInteger columnUpdatedCount,
AtomicInteger columnRemovedCount)
private void startSyncColumn(SourceEntity entity, Plugin plugin, Map<String, TableEntity> tableCache, Map<String, List<ColumnEntity>> tableColumnCache, AtomicInteger columnAddedCount, AtomicInteger columnUpdatedCount, AtomicInteger columnRemovedCount)
{
String templateName = "SYSTEM_FOR_GET_ALL_COLUMNS";
TemplateSqlEntity template = getTemplate(templateName, entity);
@ -771,43 +672,21 @@ public class SourceServiceImpl
log.error("The source [ {} ] protocol [ {} ] sync metadata columns [ {} ] failed", entity.getName(), entity.getProtocol(), response.getMessage());
}
else {
List<ColumnEntity> entities = response.getColumns()
.stream()
.map(item -> {
String key = String.format("%s_%s", getNodeText(item, NodeType.CATALOG), getNodeText(item, NodeType.SCHEMA));
TableEntity table = tableCache.get(key);
String name = getNodeText(item, NodeType.COLUMN);
ColumnEntity column = ColumnEntity.builder()
.name(name)
.description(String.format("Table [ %s ] of column [ %s ] ", table.getName(), name))
.type(getNodeText(item, NodeType.COLUMN_TYPE))
.comment(getNodeText(item, NodeType.COMMENT))
.defaultValue(getNodeText(item, NodeType.DEFAULT))
.position(getNodeText(item, NodeType.POSITION))
.maximumLength(getNodeText(item, NodeType.MAXIMUM_LENGTH))
.collation(getNodeText(item, NodeType.COLLATION))
.isKey(getNodeText(item, NodeType.KEY))
.privileges(getNodeText(item, NodeType.FORMAT))
.dataType(getNodeText(item, NodeType.DATA_TYPE))
.extra(getNodeText(item, NodeType.EXTRA))
.isNullable(getNodeText(item, NodeType.NULLABLE))
.table(table)
.build();
return column;
})
.collect(Collectors.toList());
List<ColumnEntity> entities = response.getColumns().stream().map(item -> {
String key = String.format("%s_%s", getNodeText(item, NodeType.CATALOG), getNodeText(item, NodeType.SCHEMA));
TableEntity table = tableCache.get(key);
String name = getNodeText(item, NodeType.COLUMN);
ColumnEntity column = ColumnEntity.builder().name(name).description(String.format("Table [ %s ] of column [ %s ] ", table.getName(), name)).type(getNodeText(item, NodeType.COLUMN_TYPE)).comment(getNodeText(item, NodeType.COMMENT)).defaultValue(getNodeText(item, NodeType.DEFAULT)).position(getNodeText(item, NodeType.POSITION)).maximumLength(getNodeText(item, NodeType.MAXIMUM_LENGTH)).collation(getNodeText(item, NodeType.COLLATION)).isKey(getNodeText(item, NodeType.KEY)).privileges(getNodeText(item, NodeType.FORMAT)).dataType(getNodeText(item, NodeType.DATA_TYPE)).extra(getNodeText(item, NodeType.EXTRA)).isNullable(getNodeText(item, NodeType.NULLABLE)).table(table).build();
return column;
}).collect(Collectors.toList());
Map<String, List<ColumnEntity>> groupEntities = entities
.stream()
.collect(Collectors.groupingBy(item -> String.format("%s_%s", item.getTable().getDatabase().getName(), item.getTable().getName())));
Map<String, List<ColumnEntity>> groupEntities = entities.stream().collect(Collectors.groupingBy(item -> String.format("%s_%s", item.getTable().getDatabase().getName(), item.getTable().getName())));
groupEntities.forEach((key, groupItem) -> {
// Detect data that needs to be updated
List<ColumnEntity> origin = tableColumnCache.get(key);
groupItem.forEach(item -> {
Optional<ColumnEntity> optionalColumn = origin.stream()
.filter(node -> node.getName().equals(item.getName()))
.findAny();
Optional<ColumnEntity> optionalColumn = origin.stream().filter(node -> node.getName().equals(item.getName())).findAny();
if (optionalColumn.isPresent()) {
ColumnEntity node = optionalColumn.get();
item.setId(node.getId());
@ -822,9 +701,7 @@ public class SourceServiceImpl
log.info("Added column size [ {} ] to table [ {} ]", groupItem.size(), key);
columnHandler.saveAll(groupItem);
List<ColumnEntity> deleteEntities = origin.stream()
.filter(node -> groupItem.stream().noneMatch(item -> node.getName().equals(item.getName())))
.collect(Collectors.toList());
List<ColumnEntity> deleteEntities = origin.stream().filter(node -> groupItem.stream().noneMatch(item -> node.getName().equals(item.getName()))).collect(Collectors.toList());
log.info("Removed column size [ {} ] from table [ {} ]", deleteEntities.size(), key);
columnHandler.deleteAll(deleteEntities);
columnRemovedCount.addAndGet(deleteEntities.size());

View File

@ -31,6 +31,9 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"echarts": "^5.5.0",
"embla-carousel": "^8.0.2",
"embla-carousel-autoplay": "^8.0.2",
"embla-carousel-vue": "^8.0.2",
"export-to-csv": "^1.2.4",
"lodash": "^4.17.21",
"lucide-vue-next": "^0.356.0",

View File

@ -0,0 +1,44 @@
<script setup lang="ts">
import { useProvideCarousel } from './useCarousel'
import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
orientation: 'horizontal',
})
const emits = defineEmits<CarouselEmits>()
const carouselArgs = useProvideCarousel(props, emits)
defineExpose(carouselArgs)
function onKeyDown(event: KeyboardEvent) {
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
if (event.key === prevKey) {
event.preventDefault()
carouselArgs.scrollPrev()
return
}
if (event.key === nextKey) {
event.preventDefault()
carouselArgs.scrollNext()
}
}
</script>
<template>
<div
:class="cn('relative', props.class)"
role="region"
aria-roledescription="carousel"
tabindex="0"
@keydown="onKeyDown"
>
<slot v-bind="carouselArgs" />
</div>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { useCarousel } from './useCarousel'
import type { WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<WithClassAsProps>()
const { carouselRef, orientation } = useCarousel()
</script>
<template>
<div ref="carouselRef" class="overflow-hidden">
<div
:class="
cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
props.class,
)"
v-bind="$attrs"
>
<slot />
</div>
</div>
</template>

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import { useCarousel } from './useCarousel'
import type { WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
const props = defineProps<WithClassAsProps>()
const { orientation } = useCarousel()
</script>
<template>
<div
role="group"
aria-roledescription="slide"
:class="cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
props.class,
)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { ArrowRightIcon } from '@radix-icons/vue'
import { useCarousel } from './useCarousel'
import type { WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
const props = defineProps<WithClassAsProps>()
const { orientation, canScrollNext, scrollNext } = useCarousel()
</script>
<template>
<Button
:disabled="!canScrollNext"
:class="cn(
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
props.class,
)"
variant="outline"
@click="scrollNext"
>
<slot>
<ArrowRightIcon class="h-4 w-4 text-current" />
</slot>
</Button>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { ArrowLeftIcon } from '@radix-icons/vue'
import { useCarousel } from './useCarousel'
import type { WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
const props = defineProps<WithClassAsProps>()
const { orientation, canScrollPrev, scrollPrev } = useCarousel()
</script>
<template>
<Button
:disabled="!canScrollPrev"
:class="cn(
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
props.class,
)"
variant="outline"
@click="scrollPrev"
>
<slot>
<ArrowLeftIcon class="h-4 w-4 text-current" />
</slot>
</Button>
</template>

View File

@ -0,0 +1,10 @@
export { default as Carousel } from './Carousel.vue'
export { default as CarouselContent } from './CarouselContent.vue'
export { default as CarouselItem } from './CarouselItem.vue'
export { default as CarouselPrevious } from './CarouselPrevious.vue'
export { default as CarouselNext } from './CarouselNext.vue'
export { useCarousel } from './useCarousel'
export type {
EmblaCarouselType as CarouselApi,
} from 'embla-carousel'

View File

@ -0,0 +1,20 @@
import type {
EmblaCarouselType as CarouselApi,
EmblaOptionsType as CarouselOptions,
EmblaPluginType as CarouselPlugin,
} from 'embla-carousel'
import type { HTMLAttributes, Ref } from 'vue'
export interface CarouselProps {
opts?: CarouselOptions | Ref<CarouselOptions>
plugins?: CarouselPlugin[] | Ref<CarouselPlugin[]>
orientation?: 'horizontal' | 'vertical'
}
export interface CarouselEmits {
(e: 'init-api', payload: CarouselApi): void
}
export interface WithClassAsProps {
class?: HTMLAttributes['class']
}

View File

@ -0,0 +1,59 @@
import { createInjectionState } from '@vueuse/core'
import emblaCarouselVue from 'embla-carousel-vue'
import { onMounted, ref } from 'vue'
import type {
EmblaCarouselType as CarouselApi,
} from 'embla-carousel'
import type { CarouselEmits, CarouselProps } from './interface'
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
({
opts,
orientation,
plugins,
}: CarouselProps, emits: CarouselEmits) => {
const [emblaNode, emblaApi] = emblaCarouselVue({
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y',
}, plugins)
function scrollPrev() {
emblaApi.value?.scrollPrev()
}
function scrollNext() {
emblaApi.value?.scrollNext()
}
const canScrollNext = ref(true)
const canScrollPrev = ref(true)
function onSelect(api: CarouselApi) {
canScrollNext.value = api.canScrollNext()
canScrollPrev.value = api.canScrollPrev()
}
onMounted(() => {
if (!emblaApi.value)
return
emblaApi.value?.on('init', onSelect)
emblaApi.value?.on('reInit', onSelect)
emblaApi.value?.on('select', onSelect)
emits('init-api', emblaApi.value)
})
return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }
},
)
function useCarousel() {
const carouselState = useInjectCarousel()
if (!carouselState)
throw new Error('useCarousel must be used within a <Carousel />')
return carouselState
}
export { useCarousel, useProvideCarousel }

View File

@ -90,6 +90,8 @@ export default {
realtime: 'Realtime',
to: 'To',
work: 'Work Home',
chat: 'Chat',
avatar: 'Avatar',
tip: {
pageNotNetwork: 'Oops! Unable to connect to the network, please check if the network is normal!'
}

View File

@ -106,6 +106,12 @@ export default {
info: 'View Info',
lifeCycleColumn: 'Lifecycle columns',
lifeCycleNumber: 'Lifecycle number',
continuousBuild: 'Continuous Build',
},
validator: {
duplicateColumn: 'Column name [ $VALUE ] already exists',
specifiedColumn: 'Sort key or primary key must be specified',
specifiedName: 'Name must be specified',
},
tip: {
selectExpression: 'Please select the expression',
@ -117,5 +123,6 @@ export default {
rebuildProgress: 'Rebuilding will only progress unfinished',
lifeCycleMustDateColumn: 'The lifecycle must contain a date column',
modifyNotSupportDataPreview: 'Data preview is not supported to modify',
publishSuccess: 'Dataset [ $VALUE ] published successfully',
}
}

View File

@ -11,5 +11,6 @@ export default {
deleteAlert1: 'You are deleting a report. This action permanently deletes the report. Please be sure to confirm your actions before proceeding.',
deleteAlert2: 'Warning: This cannot be undone.',
deleteAlert3: 'To confirm, type [ $VALUE ] in the box below',
publishSuccess: 'Report [ $VALUE ] published successfully',
}
}

View File

@ -90,6 +90,8 @@ export default {
realtime: '实时',
to: '目标',
work: '工作目录',
chat: '聊天室',
avatar: '头像',
tip: {
pageNotNetwork: '哎呀!无法连接到网络,请检查网络是否正常!'
}

View File

@ -106,6 +106,12 @@ export default {
info: '查看详情',
lifeCycleColumn: '生命周期列',
lifeCycleNumber: '生命周期数',
continuousBuild: '连续构建',
},
validator: {
duplicateColumn: '列名 [ $VALUE ] 已存在',
specifiedColumn: '排序键或主键必须指定',
specifiedName: '数据集名必须指定',
},
tip: {
selectExpression: '请选择表达式',
@ -117,5 +123,6 @@ export default {
rebuildProgress: '重建只会进行未完成进度',
lifeCycleMustDateColumn: '生命周期必须包含一个日期列',
modifyNotSupportDataPreview: '修改暂不支持数据预览',
publishSuccess: '数据集 [ $VALUE ] 发布成功',
}
}

View File

@ -11,5 +11,6 @@ export default {
deleteAlert1: '您正在删除报表。此操作将永久删除报表。在继续操作之前,请务必确认您的操作。',
deleteAlert2: '警告:此操作无法撤销。',
deleteAlert3: '要确认,请在下面的框中键入 [ $VALUE ]',
publishSuccess: '报表 [ $VALUE ] 发布成功',
}
}

View File

@ -0,0 +1,21 @@
import { BaseModel } from '@/model/base.ts'
import { UserModel } from '@/model/user.ts'
export interface ChatModel
extends BaseModel
{
avatar?: string
description?: string
user?: UserModel
}
export class ChatRequest
{
public static of(): ChatModel
{
return {
avatar: undefined,
description: undefined
}
}
}

View File

@ -0,0 +1,12 @@
import { BaseModel } from '@/model/base.ts'
export interface MessageModel
extends BaseModel
{
content?: string
model?: string
type?: string
promptTokens?: number
completionTokens?: number
totalTokens?: number
}

View File

@ -22,6 +22,7 @@ export interface SourceModel
updateTime?: string
configures?: Map<string, string>
schema?: any
code?: string
}
export class SourceRequest

View File

@ -171,6 +171,14 @@ const createAdminRouter = (router: any) => {
},
component: () => import('@/views/pages/admin/dataset/DatasetInfo.vue')
},
{
path: 'dataset/info/source/:sourceCode?',
meta: {
title: 'common.dataset',
isRoot: false
},
component: () => import('@/views/pages/admin/dataset/DatasetInfo.vue')
},
{
path: 'dataset/adhoc/:code',
layout: LayoutContainer,
@ -244,6 +252,14 @@ const createAdminRouter = (router: any) => {
isRoot: false
},
component: () => import('@/views/pages/admin/pipeline/PipelineInfo.vue')
},
{
path: 'chat',
meta: {
title: 'common.chat',
isRoot: false
},
component: () => import('@/views/pages/admin/chat/ChatHome.vue')
}
]
}

View File

@ -0,0 +1,21 @@
import { BaseService } from '@/services/base'
import { ResponseModel } from '@/model/response'
import { HttpUtils } from '@/utils/http'
const DEFAULT_PATH = '/api/v1/chat'
class ChatService
extends BaseService
{
constructor()
{
super(DEFAULT_PATH)
}
getMessages(id: number): Promise<ResponseModel>
{
return new HttpUtils().get(`${ DEFAULT_PATH }/${ id }/messages`)
}
}
export default new ChatService()

View File

@ -21,6 +21,11 @@ class SourceService
return new HttpUtils().get(DEFAULT_PATH_V1, { page: page, size: size })
}
getByCode(code: string): Promise<ResponseModel>
{
return new HttpUtils().get(`${ DEFAULT_PATH_V2 }/code/${ code }`)
}
getPlugins(): Promise<ResponseModel>
{
return new HttpUtils().get(`${ DEFAULT_PATH_V1 }/plugins`)

View File

@ -0,0 +1,15 @@
export class ArrayUtils
{
static findDuplicates(array: any[]): any[]
{
const counts: { [key: string]: number } = {}
const duplicates: string[] = []
array.forEach(column => {
counts[column.name] = (counts[column.name] || 0) + 1
if (counts[column.name] === 2) {
duplicates.push(column.name)
}
})
return duplicates
}
}

View File

@ -0,0 +1,17 @@
import packageJson from '../../package.json'
interface PackageJson
{
name: string
description: string
version: string
}
export class PackageUtils
{
public static get(key: keyof PackageJson): string
{
const pg = packageJson as PackageJson
return pg[key]
}
}

View File

@ -10,4 +10,5 @@ export interface GridConfigure
context?: string
sourceId?: number
query?: string
code?: string
}

View File

@ -4,7 +4,7 @@
<CardHeader class="p-0">
<CardTitle class="pt-1">
<Button size="sm" class="ml-2">
<RouterLink to="/admin/dataset/info" target="_blank">
<RouterLink :to="`/admin/dataset/info/source/${configure.code}`" target="_blank">
<span class="flex items-center">
<Plus :size="20"/> {{ $t('common.dataset') }}
</span>
@ -101,7 +101,7 @@ export default defineComponent({
if (this.configure) {
this.updateData(this.configure)
this.configure.headers!.forEach((header: string) => {
const columnDef: GridColumn = {headerName: header, field: header}
const columnDef: GridColumn = { headerName: header, field: header }
this.columnDefs.push(columnDef)
})
}
@ -117,5 +117,5 @@ export default defineComponent({
this.isPage = value
}
}
});
})
</script>

View File

@ -0,0 +1,28 @@
<template>
<a v-if="external" :href="link as string" :target="target">
<slot></slot>
</a>
<RouterLink v-else :to="link as string" :target="target">
<slot></slot>
</RouterLink>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'DcLink',
props: {
external: {
type: Boolean
},
link: {
type: String
},
target: {
type: String,
default: '_blank'
}
}
})
</script>

View File

@ -7,7 +7,7 @@
<SelectContent>
<Loader2 v-if="loading" class="w-full justify-center animate-spin"/>
<SelectGroup v-else>
<SelectItem v-for="item in items" :value="`${item.id}:${item.type}`" :disabled="!item.available">
<SelectItem v-for="item in items" :value="`${item.id}:${item.type}:${item.code}`" :disabled="!item.available">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{{ `${item.name} (${item.protocol})` }}</TooltipTrigger>

View File

@ -3,9 +3,10 @@
<div class="hidden flex-col md:flex">
<LayoutHeader/>
<LayoutBreadcrumb/>
<div class="flex-1 space-y-4 pl-8 pr-8">
<RouterView />
<div class="flex-1 space-y-4 pl-8 pr-8 min-h-[700px]">
<RouterView/>
</div>
<LayoutFooter :data="footers"/>
</div>
</div>
</template>
@ -16,15 +17,23 @@ import LayoutHeader from '@/views/layouts/common/components/LayoutHeader.vue'
import { Button } from '@/components/ui/button'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import LayoutBreadcrumb from '@/views/layouts/common/components/LayoutBreadcrumb.vue'
import LayoutFooter from '@/views/layouts/common/components/LayoutFooter.vue'
import { TokenUtils } from '@/utils/token'
import { ObjectUtils } from '@/utils/object'
import { HttpUtils } from '@/utils/http'
import UserService from '@/services/user'
import CommonUtils from '@/utils/common'
import { FooterModel } from '@/views/layouts/common/components/model/footer.ts'
export default defineComponent({
name: 'LayoutContainer',
components: {LayoutBreadcrumb, AvatarFallback, AvatarImage, Avatar, Button, LayoutHeader},
components: {
LayoutBreadcrumb,
AvatarFallback, AvatarImage, Avatar,
Button,
LayoutHeader,
LayoutFooter
},
beforeUnmount()
{
clearInterval(this.timer)
@ -32,12 +41,14 @@ export default defineComponent({
data()
{
return {
timer: null as any
timer: null as any,
footers: [] as Array<FooterModel>
}
},
created()
{
this.handlerInitialize()
this.handlerInitializeFooter()
},
methods: {
handlerInitialize()
@ -46,18 +57,109 @@ export default defineComponent({
if (ObjectUtils.isNotEmpty(user)) {
this.timer = setInterval(() => {
const runTime = new Date().toLocaleTimeString()
console.log(`[DataCap] refresh on time ${runTime}`)
console.log(`[DataCap] refresh on time ${ runTime }`)
const client = new HttpUtils().getAxios()
client.all([UserService.getMenus(), UserService.getInfo()])
.then(client.spread((fetchMenu, fetchInfo) => {
if (fetchMenu.status && fetchInfo.status) {
localStorage.setItem(CommonUtils.menu, JSON.stringify(fetchMenu.data))
localStorage.setItem(CommonUtils.userEditorConfigure, JSON.stringify(fetchInfo.data.editorConfigure))
}
}))
.then(client.spread((fetchMenu, fetchInfo) => {
if (fetchMenu.status && fetchInfo.status) {
localStorage.setItem(CommonUtils.menu, JSON.stringify(fetchMenu.data))
localStorage.setItem(CommonUtils.userEditorConfigure, JSON.stringify(fetchInfo.data.editorConfigure))
}
}))
}, 1000 * 60)
}
},
handlerInitializeFooter()
{
const footers = new Array<FooterModel>()
footers.push({
title: 'Resources',
children: [
{
title: 'Blog',
link: 'https://datacap.devlive.org/',
external: true,
blank: '_blank'
},
{
title: 'Gitee',
link: 'https://gitee.com/devlive-community/datacap',
external: true,
blank: '_blank'
},
{
title: 'Github',
link: 'https://github.com/devlive-community/datacap',
external: true,
blank: '_blank'
},
{
title: 'Documentation',
link: 'https://datacap.devlive.org/',
external: true,
blank: '_blank'
}
]
})
footers.push({
title: 'Community',
children: [
{
title: 'Website',
link: 'https://datacap.devlive.org/',
external: true,
blank: '_blank'
},
{
title: 'Issues',
link: 'https://github.com/devlive-community/datacap/issues',
external: true,
blank: '_blank'
},
{
title: 'Discussions',
link: 'https://github.com/devlive-community/datacap/discussions',
external: true,
blank: '_blank'
}
]
})
footers.push({
title: 'About',
children: [
{
title: 'DataCap',
link: 'https://datacap.devlive.org/',
external: true,
blank: '_blank'
}
]
})
footers.push({
title: 'Projects',
children: [
{
title: 'Database Tools',
link: 'https://github.com/devlive-community/dbm',
external: true,
blank: '_blank'
},
{
title: 'Open AI Java SDK',
link: 'https://github.com/devlive-community/openai-java-sdk',
external: true,
blank: '_blank'
},
{
title: 'Shadcn UI Vue Admin',
link: 'https://github.com/devlive-community/shadcn-ui-vue-admin',
external: true,
blank: '_blank'
}
]
})
this.footers = footers
}
}
});
})
</script>

View File

@ -0,0 +1,51 @@
<template>
<footer class="font-sans py-8 px-10 mt-5">
<div :class="`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-${data.length} gap-8`">
<div v-for="item in data" :key="item.title">
<h4 class="text-[#808695] font-bold text-lg mb-5">{{ item.title }}</h4>
<ul class="space-y-4">
<li v-for="children in item.children" :key="children.title">
<DcLink :external="children.external" :link="children.link" :target="children.blank"
class="hover:text-[#5cadff] text-[#808695] text-[15px] font-semibold transition-all">
{{ children.title }}
</DcLink>
</li>
</ul>
</div>
</div>
<div class="border-t text-center border-[#dcdee2] pt-8 mt-8 space-y-2">
<p class="text-gray-300 text-[15px] font-semibold">
Copyright © 2022 - {{ new Date().getFullYear() }} Devlive Community All Rights Reserved
</p>
<p>{{ $t('common.version') }}:
<Text type="danger"
strong>
{{ version }}
</Text>
</p>
</div>
</footer>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { FooterModel } from '@/views/layouts/common/components/model/footer.ts'
import DcLink from '@/views/components/link/DcLink.vue'
import { PackageUtils } from '@/utils/package.ts'
export default defineComponent({
name: 'LayoutFooter',
components: { DcLink },
props: {
data: {
type: Array as () => Array<FooterModel>,
default: () => new Array<FooterModel>()
}
},
setup()
{
const version = PackageUtils.get('version')
return { version }
}
})
</script>

View File

@ -1,4 +1,5 @@
<template>
<Carousel :items="carouselItems" :delay="3000"/>
<header class="sticky z-40 top-0 bg-background/80 backdrop-blur-lg border-b border-border">
<div class="container flex h-14 max-w-screen-2xl items-center">
<div class="container flex h-14 items-center justify-between">
@ -136,6 +137,7 @@ import NavigationMenuListItem from '@/views/layouts/common/components/components
import { CircleHelp, LogOut, Settings } from 'lucide-vue-next'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import LanguageSwitcher from '@/views/layouts/common/components/components/LanguageSwitcher.vue'
import Carousel from '@/views/ui/carousel'
interface NavigationItem
{
@ -171,14 +173,25 @@ export default defineComponent({
router.push('/auth/signin')
}
const carouselItems = [
{
title: 'Support ChatGPT',
link: '/admin/chat',
external: false,
isAlert: true
}
]
return {
userInfo,
isLoggedIn,
activeMenus,
logout
logout,
carouselItems
}
},
components: {
Carousel,
LanguageSwitcher,
TooltipContent, Tooltip, TooltipTrigger, TooltipProvider,
NavigationMenuLink, NavigationMenuContent, NavigationMenuTrigger, NavigationMenuItem, NavigationMenuList, NavigationMenu,
@ -196,6 +209,6 @@ export default defineComponent({
this.$emit('changeLanguage', language)
}
}
});
})
</script>

View File

@ -0,0 +1,10 @@
export interface FooterModel
{
title?: string
icon?: string
link?: string
external?: boolean
children?: FooterModel[],
copyright?: string
blank?: string
}

View File

@ -5,7 +5,7 @@
<aside class="-mx-4 lg:w-1/12">
<LayoutSidebar/>
</aside>
<div class="flex-1 lg:max-w-3xl">
<div class="flex-1 lg:max-w-5xl">
<div class="space-y-6">
<RouterView/>
</div>

View File

@ -0,0 +1,245 @@
<template>
<div class="hidden space-y-6 pb-16 w-full md:block">
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-6 lg:space-y-0">
<aside class="-mx-4 w-[200px]">
<Card title-class="p-2" body-class="p-2">
<template #title>{{ $t('common.chat') }}</template>
<template #extra>
<Button size="icon" class="w-6 h-6 rounded-full" @click="handlerInfo(true)">
<Plus :size="15"/>
</Button>
</template>
<CircularLoading v-if="loading" :show="loading"/>
<div v-else>
<FormField type="radio" name="theme">
<FormItem class="space-y-1">
<RadioGroup class="grid gap-3 pt-2 cursor-pointer" @update:modelValue="handlerChange">
<FormItem v-for="item of data" :key="item.id">
<FormLabel class="[&:has([data-state=checked])>div]:border-primary cursor-pointer">
<FormControl>
<RadioGroupItem :value="item.id as unknown as string" class="sr-only cursor-pointer"/>
</FormControl>
<div class="items-center rounded-md border-4 border-muted p-1 hover:border-accent">
<div class="flex flex-row items-center justify-between">
<div class="flex items-center space-x-4">
<Avatar :src="item.avatar" :alt="item.name"/>
<div>
<p class="text-sm font-medium leading-none">{{ item.name }}</p>
</div>
</div>
</div>
</div>
</FormLabel>
</FormItem>
</RadioGroup>
</FormItem>
</FormField>
</div>
</Card>
</aside>
<div class="flex-1">
<div v-if="dataInfo">
<Card title-class="p-2" body-class="p-2">
<template #title>
<div class="flex flex-row items-center justify-between">
<div class="flex items-center space-x-4">
<Avatar :src="dataInfo.avatar" :alt="dataInfo.name"/>
<div>
<p class="text-sm font-medium leading-none">{{ dataInfo.name }}</p>
<p class="text-sm text-muted-foreground">{{ dataInfo.description }}</p>
</div>
</div>
</div>
</template>
<template #extra>
<div class="flex h-5 items-center space-x-4 text-sm">
<div>
Prompt Tokens: {{ promptTokens }}
</div>
<Separator orientation="vertical"/>
<div>
Completion Tokens: {{ completionTokens }}
</div>
<Separator orientation="vertical"/>
<div>
Total Tokens: {{ totalTokens }}
</div>
</div>
</template>
<CircularLoading v-if="loadingMessages" :show="loadingMessages"/>
<div ref="scrollDiv" class="space-y-4 h-[500px] overflow-y-auto">
<div v-for="(item, index) in messages" :key="index">
<div
:class="cn( 'flex w-max max-w-[75%] flex-col gap-2 rounded-lg px-3 py-2 text-sm', item.type === 'question' ? 'ml-auto bg-primary text-primary-foreground' : 'bg-muted')">
{{ item.content }}
</div>
<div v-if="item.type === 'answer'" class="flex text-sm text-muted-foreground mt-0.5 space-x-2">
<div>Model: {{ item.model }}</div>
<Separator orientation="vertical"/>
<div>Prompt Tokens: {{ item.promptTokens }}</div>
<Separator orientation="vertical"/>
<div>Completion Tokens: {{ item.completionTokens }}</div>
<Separator orientation="vertical"/>
<div>Total Tokens: {{ item.totalTokens }}</div>
</div>
</div>
</div>
<template #footer>
<div class="flex w-full items-center space-x-2">
<Input v-model="inputValue" :disabled="submitting" placeholder="Type a message ..." class="flex-1"/>
<Button class="p-2.5 flex items-center justify-center" :loading="submitting" :disabled="!inputValue || submitting" @click="handlerSubmit">
<Send v-if="!submitting" class="w-4 h-4"/>
<span class="sr-only"></span>
</Button>
</div>
</template>
</Card>
</div>
</div>
</div>
</div>
<ChatInfo v-if="dataVisible" :is-visible="dataVisible" @close="handlerInfo($event)"/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import CircularLoading from '@/views/components/loading/CircularLoading.vue'
import { FilterModel } from '@/model/filter.ts'
import ChatService from '@/services/chat.ts'
import { ToastUtils } from '@/utils/toast.ts'
import Card from '@/views/ui/card'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import Button from '@/views/ui/button'
import { Plus, Send } from 'lucide-vue-next'
import ChatInfo from '@/views/pages/admin/chat/ChatInfo.vue'
import Avatar from '@/views/ui/avatar'
import { ChatModel } from '@/model/chat.ts'
import { toNumber } from 'lodash'
import { cn } from '@/lib/utils.ts'
import { MessageModel } from '@/model/message.ts'
import { Input } from '@/components/ui/input'
import MessageService from '@/services/message.ts'
import { Separator } from '@/components/ui/separator'
export default defineComponent({
name: 'ChatHome',
components: {
Separator,
Input,
Avatar,
ChatInfo,
Button,
CircularLoading,
Card,
FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,
RadioGroup, RadioGroupItem,
Plus, Send
},
setup()
{
const filter: FilterModel = new FilterModel()
return {
filter,
cn
}
},
data()
{
return {
loading: true,
data: [] as ChatModel[],
dataInfo: null as ChatModel | null,
dataVisible: false,
loadingMessages: false,
messages: [] as MessageModel[],
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
submitting: false,
inputValue: ''
}
},
created()
{
this.handlerInitialize()
},
methods: {
handlerInitialize()
{
this.loading = true
ChatService.getAll(this.filter)
.then(response => {
if (response.status) {
this.data = response.data.content
}
else {
ToastUtils.error(response.message)
}
})
.finally(() => this.loading = false)
},
handlerInfo(opened: boolean)
{
this.dataVisible = opened
if (!opened) {
this.handlerInitialize()
}
},
handlerChange(value: string)
{
this.dataInfo = this.data.find(item => item.id === toNumber(value)) as unknown as ChatModel
this.loadingMessages = true
ChatService.getMessages(toNumber(value))
.then(response => {
this.messages = response.data
})
.finally(() => {
this.loadingMessages = false
this.counterToken()
this.handlerGoBottom()
})
},
handlerSubmit()
{
this.submitting = true
const message = {
content: this.inputValue,
chat: this.dataInfo,
type: 'question'
}
this.messages.push(message)
this.handlerGoBottom()
MessageService.saveOrUpdate(message)
.then(response => {
if (response.status) {
this.messages.push(response.data)
this.inputValue = ''
}
else {
this.$Message.error(response.message)
}
})
.finally(() => {
this.submitting = false
this.counterToken()
this.handlerGoBottom()
})
},
handlerGoBottom()
{
const scrollElem = this.$refs.scrollDiv as any
setTimeout(() => {
scrollElem.scrollTo({ top: scrollElem.scrollHeight, behavior: 'smooth' })
}, 0)
},
counterToken()
{
const answers = this.messages.filter(message => message.type === 'answer')
this.promptTokens = answers.reduce((sum, message) => sum + toNumber(message.promptTokens), 0)
this.completionTokens = answers.reduce((sum, message) => sum + toNumber(message.completionTokens), 0)
this.totalTokens = answers.reduce((sum, message) => sum + toNumber(message.totalTokens), 0)
}
}
})
</script>

View File

@ -0,0 +1,113 @@
<template>
<Dialog :is-visible="visible" :title="$t('common.chat')">
<div class="space-y-2 pl-3 pr-3">
<FormField name="name">
<FormItem class="space-y-1">
<FormLabel>{{ $t('common.name') }}</FormLabel>
<FormMessage/>
<Input v-model="formState.name"/>
</FormItem>
</FormField>
<FormField name="avatar">
<FormItem class="space-y-1">
<FormLabel>{{ $t('common.avatar') }}</FormLabel>
<FormMessage/>
<Input v-model="formState.avatar"/>
</FormItem>
</FormField>
<FormField name="description">
<FormItem class="space-y-1">
<FormLabel>{{ $t('common.description') }}</FormLabel>
<FormMessage/>
<Textarea v-model="formState.description"/>
</FormItem>
</FormField>
</div>
<template #footer>
<div class="space-x-5">
<Button variant="outline" size="sm" @click="handlerCancel">
{{ $t('common.cancel') }}
</Button>
<Button size="sm" :loading="loading" :disabled="loading" @click="handlerSave()">
{{ $t('common.save') }}
</Button>
</div>
</template>
</Dialog>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Dialog from '@/views/ui/dialog'
import Button from '@/views/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { ToastUtils } from '@/utils/toast'
import ChatService from '@/services/chat.ts'
import CircularLoading from '@/views/components/loading/CircularLoading.vue'
import { ChatModel, ChatRequest } from '@/model/chat.ts'
export default defineComponent({
name: 'ChatInfo',
components: {
CircularLoading,
Input,
FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,
Textarea,
Button,
Dialog
},
computed: {
visible: {
get(): boolean
{
return this.isVisible
},
set(value: boolean)
{
this.$emit('close', value)
}
}
},
props: {
isVisible: {
type: Boolean
}
},
data()
{
return {
loading: false,
formState: null as unknown as ChatModel
}
},
created()
{
this.formState = ChatRequest.of()
},
methods: {
handlerSave()
{
if (this.formState) {
this.loading = true
ChatService.saveOrUpdate(this.formState)
.then(response => {
if (response.status) {
ToastUtils.success(this.$t('common.success'))
this.handlerCancel()
}
else {
ToastUtils.error(response.message)
}
})
.finally(() => this.loading = false)
}
},
handlerCancel()
{
this.visible = false
}
}
})
</script>

View File

@ -38,8 +38,11 @@
<p class="text-xs text-muted-foreground mt-2">{{ item.createTime }}</p>
</Card>
</div>
<div v-if="data.length === 0" class="text-center">
{{ $t('common.noData') }}
</div>
<div>
<Pagination :pagination="pagination" @changePage="handlerChangePage"/>
<Pagination v-if="pagination && !loading && data.length > 0" :pagination="pagination" @changePage="handlerChangePage"/>
</div>
</div>
</div>
@ -60,10 +63,12 @@ import DashboardDelete from '@/views/pages/admin/dashboard/DashboardDelete.vue'
import Card from '@/views/ui/card'
import Pagination from '@/views/ui/pagination'
import Button from '@/views/ui/button'
import { TableCaption } from '@/components/ui/table'
export default defineComponent({
name: 'DashboardHome',
components: {
TableCaption,
Pagination,
DashboardDelete,
DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuContent, DropdownMenuTrigger, DropdownMenu,

View File

@ -21,6 +21,10 @@
</RadioGroup>
</FormItem>
</FormField>
<div v-if="data.length === 0" class="flex w-full items-center">
{{ $t('common.noData') }}
</div>
<Pagination v-if="pagination && !loading && data.length > 0" :pagination="pagination" @changePage="handlerChangePage"/>
</div>
<template #footer>
<div class="space-x-5">
@ -47,10 +51,13 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import VisualView from '@/views/components/visual/VisualView.vue'
import Button from '@/views/ui/button'
import { toNumber } from 'lodash'
import Pagination from '@/views/ui/pagination'
import { PaginationModel, PaginationRequest } from '@/model/pagination.ts'
export default defineComponent({
name: 'ChartContainer',
components: {
Pagination,
VisualView,
CircularLoading,
Dialog,
@ -78,6 +85,7 @@ export default defineComponent({
setup()
{
const filter: FilterModel = new FilterModel()
filter.size = 12
return {
filter
@ -92,7 +100,8 @@ export default defineComponent({
return {
loading: false,
data: [] as ReportModel[],
report: ''
report: '',
pagination: {} as PaginationModel
}
},
methods: {
@ -103,10 +112,17 @@ export default defineComponent({
.then(response => {
if (response.status) {
this.data = response.data.content
this.pagination = PaginationRequest.of(response.data)
}
})
.finally(() => this.loading = false)
},
handlerChangePage(value: PaginationModel)
{
this.filter.page = value.currentPage
this.filter.size = value.pageSize
this.handlerInitialize()
},
handlerSave()
{
const node = this.data.find(item => item.id === toNumber(this.report))

View File

@ -42,9 +42,14 @@
</FormField>
</div>
<template #footer>
<Button :loading="loading" @click="handlerSave">
{{ $t('common.save') }}
</Button>
<div class="space-x-5">
<Button variant="outline" size="sm" @click="configureVisible = false">
{{ $t('common.cancel') }}
</Button>
<Button :loading="loading" size="sm" @click="handlerSave">
{{ $t('common.save') }}
</Button>
</div>
</template>
</Dialog>
</div>

View File

@ -1,14 +1,11 @@
<template>
<div class="hidden space-y-6 pb-16 w-full h-full md:block">
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside class="-mx-4 w-[200px]">
<Card>
<CardHeader class="p-3 border-b">
<CardTitle>
{{ $t('dataset.common.columnModeMetric') }}
</CardTitle>
</CardHeader>
<CardContent class="p-3">
<aside class="-mx-4 w-[200px] space-y-5">
<Card body-class="p-3" title-class="p-2">
<template #title>{{ $t('dataset.common.columnModeMetric') }}</template>
<CircularLoading v-if="initialize" :show="initialize"/>
<div v-else>
<Draggable item-key="id" :clone="handlerClone" :group="{ name: 'metrics', pull: 'clone', put: false }" :list="originalMetrics">
<template #item="{ element }">
<Badge variant="outline" class="cursor-pointer mr-1">
@ -16,15 +13,12 @@
</Badge>
</template>
</Draggable>
</CardContent>
</div>
</Card>
<Card class="mt-5">
<CardHeader class="p-3 border-b">
<CardTitle>
{{ $t('dataset.common.columnModeDimension') }}
</CardTitle>
</CardHeader>
<CardContent class="p-3">
<Card body-class="p-3" title-class="p-2">
<template #title>{{ $t('dataset.common.columnModeDimension') }}</template>
<CircularLoading v-if="initialize" :show="initialize"/>
<div v-else>
<Draggable item-key="id" :clone="handlerClone" :group="{ name: 'dimensions', pull: 'clone', put: false }" :list="originalDimensions">
<template #item="{ element }">
<Badge variant="outline" class="cursor-pointer mr-1 mt-1">
@ -32,7 +26,7 @@
</Badge>
</template>
</Draggable>
</CardContent>
</div>
</Card>
</aside>
<div class="flex-1">
@ -297,6 +291,18 @@
</div>
</FormItem>
</FormField>
<FormField class="flex items-center" name="build">
<FormItem class="flex-1">
<div class="flex items-center">
<FormLabel class="mr-1 w-20 text-right">
{{ $t('dataset.common.continuousBuild') }}
</FormLabel>
<FormControl>
<Switch :value="formState.build" @changeValue="formState.build = $event"/>
</FormControl>
</div>
</FormItem>
</FormField>
<AlertDialogFooter class="-mb-4 border-t pt-2">
<Button @click="formState.visible = false">{{ $t('common.cancel') }}</Button>
<Button :disabled="!formState.name" @click="handlerPublish">
@ -329,7 +335,7 @@ import DatasetVisualConfigureBar from '@/views/pages/admin/dataset/components/ad
import DatasetVisualConfigureLine from '@/views/pages/admin/dataset/components/adhoc/DatasetVisualConfigureLine.vue'
import { defineComponent } from 'vue'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import Card from '@/views/ui/card'
import { AreaChart, BarChart4, BarChartHorizontal, Baseline, CirclePlay, Cog, Eye, LineChart, Loader2, PieChart, Table, Trash } from 'lucide-vue-next'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Separator } from '@/components/ui/separator'
@ -343,6 +349,7 @@ import SqlInfo from '@/views/components/sql/SqlInfo.vue'
import { AlertDialog, AlertDialogContent, AlertDialogFooter, AlertDialogHeader } from '@/components/ui/alert-dialog'
import { FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form'
import { Select } from '@/components/ui/select'
import Switch from '@/views/ui/switch'
export default defineComponent({
name: 'DatasetAdhoc',
@ -357,6 +364,7 @@ export default defineComponent({
}
},
components: {
Switch,
FormField,
FormControl,
FormLabel, Select, FormItem,
@ -369,7 +377,7 @@ export default defineComponent({
Input,
Tooltip, TooltipContent, TooltipTrigger, TooltipProvider,
Separator,
CardTitle, CardHeader, CardContent, Card,
Card,
Badge,
DatasetVisualConfigureWordCloud,
DatasetVisualConfigureHistogram,
@ -415,9 +423,11 @@ export default defineComponent({
commitOptions: null,
formState: {
visible: false,
name: ''
name: '',
build: false
},
published: false
published: false,
initialize: false
}
},
created()
@ -429,37 +439,39 @@ export default defineComponent({
handlerInitialize()
{
setTimeout(() => {
this.initialize = true
const code = this.$route.params.code as string
this.code = code as string
const id = this.$route.params.id
this.id = id as unknown as number
DatasetService.getColumnsByCode(this.code)
.then(response => {
if (response.status) {
this.originalData = response.data
this.originalMetrics = response.data.filter((item: { mode: string; }) => item.mode === 'METRIC')
this.originalDimensions = response.data.filter((item: { mode: string; }) => item.mode === 'DIMENSION')
if (id) {
ReportService.getById(this.id as number)
.then(response => {
if (response.status) {
this.formState.name = response.data.name
const query = JSON.parse(response.data.query)
this.mergeColumns(query.columns, this.metrics, ColumnType.METRIC)
this.mergeColumns(query.columns, this.dimensions, ColumnType.DIMENSION)
this.mergeColumns(query.columns, this.filters, ColumnType.FILTER)
this.configure.columns = query.columns
this.configure.limit = query.limit
this.configuration = JSON.parse(response.data.configure)
this.handlerApplyAdhoc()
this.originalData = response.data
this.originalMetrics = response.data.filter((item: { mode: string; }) => item.mode === 'METRIC')
this.originalDimensions = response.data.filter((item: { mode: string; }) => item.mode === 'DIMENSION')
if (id) {
ReportService.getById(this.id as number)
.then(response => {
if (response.status) {
this.formState.name = response.data.name
const query = JSON.parse(response.data.query)
this.mergeColumns(query.columns, this.metrics, ColumnType.METRIC)
this.mergeColumns(query.columns, this.dimensions, ColumnType.DIMENSION)
this.mergeColumns(query.columns, this.filters, ColumnType.FILTER)
this.configure.columns = query.columns
this.configure.limit = query.limit
this.configuration = JSON.parse(response.data.configure)
this.handlerApplyAdhoc()
}
})
}
}
else {
ToastUtils.error(response.message)
}
})
}
}
else {
ToastUtils.error(response.message)
}
})
.finally(() => this.initialize = false)
}, 0)
},
handlerApplyAdhoc()
@ -474,27 +486,27 @@ export default defineComponent({
this.isPublish = true
this.loading = true
DatasetService.adhoc(this.code as string, this.configure)
.then(response => {
if (response.status) {
if (this.configuration) {
if (response.data.isSuccessful) {
this.configuration.headers = response.data.headers
this.configuration.columns = response.data.columns
this.showSql.content = response.data.content
this.configuration.message = null
}
else {
this.configuration.headers = []
this.configuration.columns = []
this.configuration.message = response.data.message
}
}
}
else {
ToastUtils.error(response.message)
}
})
.finally(() => this.loading = false)
.then(response => {
if (response.status) {
if (this.configuration) {
if (response.data.isSuccessful) {
this.configuration.headers = response.data.headers
this.configuration.columns = response.data.columns
this.showSql.content = response.data.content
this.configuration.message = null
}
else {
this.configuration.headers = []
this.configuration.columns = []
this.configuration.message = response.data.message
}
}
}
else {
ToastUtils.error(response.message)
}
})
.finally(() => this.loading = false)
},
handlerClone(value: any)
{
@ -567,14 +579,16 @@ export default defineComponent({
configure.id = this.id
}
ReportService.saveOrUpdate(configure)
.then(response => {
if (response.status) {
ToastUtils.success(this.$t('report.common.publishSuccess').replace('REPLACE_NAME', this.formState.name))
this.formState.visible = false
router.push('/admin/report')
}
})
.finally(() => this.published = false)
.then(response => {
if (response.status) {
ToastUtils.success(this.$t('report.tip.publishSuccess').replace('$VALUE', this.formState.name))
this.formState.visible = false
if (!this.formState.build) {
router.push('/admin/report')
}
}
})
.finally(() => this.published = false)
},
mergeColumns(originalColumns: any[], array: any[], type?: ColumnType)
{

View File

@ -1,260 +1,275 @@
<template>
<div class="flex flex-col">
<div class="flex flex-col space-y-1">
<div class="flex justify-end">
<Button size="sm" @click="configureVisible = true">
{{ $t('common.configure') }}
</Button>
</div>
<div v-if="data || code" class="mt-3">
<AgGridVue v-if="data?.columns" :style="{height: '300px'}" class="ag-theme-datacap" :pagination="true" :columnDefs="columnDefs" :rowData="data.columns"
:gridOptions="gridOptions as any"/>
<Alert v-else variant="destructive">
{{ i18n.t('dataset.tip.modifyNotSupportDataPreview') }}
</Alert>
<Sheet :default-open="configureVisible" :open="configureVisible" @update:open="configureVisible = false">
<SheetContent side="bottom" class="w-full h-[80%]">
<SheetHeader class="border-b pb-3">
<SheetTitle>
{{ $t('common.configure') }}
<Button size="sm" class="float-right mr-5 -mt-2" @click="handlerCreate">
{{ code ? $t('dataset.common.modify') : $t('dataset.common.create') }}
</Button>
</SheetTitle>
</SheetHeader>
<Alert v-if="validator" variant="destructive">{{ validatorMessage }}</Alert>
<Tabs default-value="columns" class="mt-1">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="columns">{{ $t('dataset.common.dataColumn') }}</TabsTrigger>
<TabsTrigger value="configure">{{ $t('dataset.common.dataConfigure') }}</TabsTrigger>
</TabsList>
<TabsContent value="columns">
<CircularLoading v-if="loading" :show="loading"/>
<div v-else class="flex w-full flex-col">
<div class="flex flex-1 flex-col gap-4 p-1 text-center">
<div class="grid items-center gap-3 md:grid-cols-2 md:gap-4 lg:grid-cols-12">
<div>{{ $t('dataset.common.columnName') }}</div>
<div>{{ $t('dataset.common.columnAlias') }}</div>
<div>{{ $t('dataset.common.columnType') }}</div>
<div>{{ $t('dataset.common.columnMode') }}</div>
<div>{{ $t('dataset.common.columnDefaultValue') }}</div>
<div>{{ $t('dataset.common.columnIsNullable') }}</div>
<div>{{ $t('dataset.common.columnIsOrderByKey') }}</div>
<div>{{ $t('dataset.common.columnIsPartitionKey') }}</div>
<div>{{ $t('dataset.common.columnIsPrimaryKey') }}</div>
<div>{{ $t('dataset.common.columnIsSampling') }}</div>
<div>{{ $t('dataset.common.columnLength') }}</div>
<div>{{ $t('common.action') }}</div>
<CircularLoading v-if="loading" :show="loading"/>
<div v-else>
<Card v-if="sourceInfo" title-class="p-2" body-class="p-0">
<template #title>
<Button size="sm" :loading="running" :disabled="running" @click="handlerRun()">
{{ $t('query.common.execute') }}
</Button>
</template>
<FormField v-slot="{ componentField }" name="content">
<FormItem>
<FormControl>
<AceEditor :value="value" v-bind="componentField" @update:value="value = $event"/>
</FormControl>
</FormItem>
</FormField>
</Card>
<div v-if="data || code" class="mt-3">
<CircularLoading v-if="running" :show="running"/>
<AgGridVue v-else-if="data?.data.columns" :style="{height: '300px'}" class="ag-theme-datacap" :pagination="true" :columnDefs="columnDefs" :rowData="data.data.columns"
:gridOptions="gridOptions as any"/>
<Sheet :default-open="configureVisible" :open="configureVisible" @update:open="configureVisible = false">
<SheetContent side="bottom" class="w-full h-[80%]">
<SheetHeader class="border-b pb-3">
<SheetTitle>
{{ $t('common.configure') }}
<Button size="sm" class="float-right mr-5 -mt-2" @click="handlerCreate">
{{ code ? $t('dataset.common.modify') : $t('dataset.common.create') }}
</Button>
</SheetTitle>
</SheetHeader>
<Alert v-if="validator" variant="destructive" class="mt-2">{{ validatorMessage }}</Alert>
<Tabs default-value="columns" class="mt-1">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="columns">{{ $t('dataset.common.dataColumn') }}</TabsTrigger>
<TabsTrigger value="configure">{{ $t('dataset.common.dataConfigure') }}</TabsTrigger>
</TabsList>
<TabsContent value="columns">
<CircularLoading v-if="loading" :show="loading"/>
<div v-else class="flex w-full flex-col">
<div class="flex flex-1 flex-col gap-4 p-1 text-center">
<div class="grid items-center gap-3 md:grid-cols-2 md:gap-4 lg:grid-cols-12">
<div>{{ $t('dataset.common.columnName') }}</div>
<div>{{ $t('dataset.common.columnAlias') }}</div>
<div>{{ $t('dataset.common.columnType') }}</div>
<div>{{ $t('dataset.common.columnMode') }}</div>
<div>{{ $t('dataset.common.columnDefaultValue') }}</div>
<div>{{ $t('dataset.common.columnIsNullable') }}</div>
<div>{{ $t('dataset.common.columnIsOrderByKey') }}</div>
<div>{{ $t('dataset.common.columnIsPartitionKey') }}</div>
<div>{{ $t('dataset.common.columnIsPrimaryKey') }}</div>
<div>{{ $t('dataset.common.columnIsSampling') }}</div>
<div>{{ $t('dataset.common.columnLength') }}</div>
<div>{{ $t('common.action') }}</div>
</div>
<div class="grid gap-3 md:grid-cols-2 md:gap-3 lg:grid-cols-12 h-[480px] overflow-y-auto pt-2 pb-2">
<template v-for="(item, index) in formState.columns" :key="index">
<div>
<Input v-model="item.name" type="text"/>
</div>
<div>
<Input v-model="item.aliasName" type="text"/>
</div>
<div>
<Select v-model="item.type">
<SelectTrigger>
<SelectValue placeholder="Select a fruit"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="STRING">{{ $t('dataset.common.columnTypeString') }}</SelectItem>
<SelectItem value="NUMBER">{{ $t('dataset.common.columnTypeNumber') }}</SelectItem>
<SelectItem value="NUMBER_SIGNED">{{ $t('dataset.common.columnTypeNumberSigned') }}</SelectItem>
<SelectItem value="BOOLEAN">{{ $t('dataset.common.columnTypeBoolean') }}</SelectItem>
<SelectItem value="DATETIME">{{ $t('dataset.common.columnTypeDateTime') }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="ml-4">
<div class="flex items-center space-x-2 mt-2">
<Label for="airplane-mode">{{ $t('dataset.common.columnModeMetric') }}</Label>
<Switch v-model="item.mode" :default-checked="item.mode === 'DIMENSION'" @update:checked="setMode(item, $event)"/>
<Label for="airplane-mode">{{ $t('dataset.common.columnModeDimension') }}</Label>
</div>
</div>
<div>
<Input v-model="item.defaultValue" type="text" :disabled="item.virtualColumn"/>
</div>
<div class="mt-2">
<Switch v-model="item.nullable" :disabled="item.virtualColumn" :default-checked="item.nullable"
@update:checked="setNullable(item, $event)"/>
</div>
<div class="mt-2 ml-4">
<Switch v-model="item.orderByKey" :default-checked="item.orderByKey" :disabled="item.virtualColumn"
@update:checked="setOrderByKey(item, $event)"/>
</div>
<div class="mt-2 ml-4">
<Switch v-model="item.partitionKey" :disabled="item.virtualColumn" :default-checked="item.partitionKey"
@update:checked="setPartitionKey(item, $event)"/>
</div>
<div class="mt-2 ml-4">
<Switch v-model="item.primaryKey" :disabled="item.virtualColumn" :default-checked="item.primaryKey"
@update:checked="setPrimaryKey(item, $event)"/>
</div>
<div class="mt-2 ml-4">
<Switch v-model="item.samplingKey" :disabled="item.virtualColumn" :default-checked="item.samplingKey"
@update:checked="setSamplingKey(item, $event)"/>
</div>
<div>
<Input v-model="item.length" type="number" :disabled="item.type === 'BOOLEAN' || item.type === 'DATETIME' || item.virtualColumn"/>
</div>
<div class="space-x-1 ml-4">
<Popover>
<PopoverTrigger as-child>
<Button class="rounded-full w-8 h-8" variant="outline" size="icon">
<Pencil :size="15"/>
</Button>
</PopoverTrigger>
<PopoverContent class="w-80">
<div class="grid gap-4">
<div class="space-y-2">
<h4 class="font-medium leading-none">{{ $t('dataset.common.columnComment') }}</h4>
</div>
<Textarea v-model="item.comment"/>
</div>
</PopoverContent>
</Popover>
<Button class="rounded-full w-8 h-8" variant="destructive" size="icon" :disabled="!item.customColumn" @click="handlerRemoveColumn(index)">
<Trash :size="15"/>
</Button>
<Button class="rounded-full w-8 h-8" size="icon" @click="handlerAddColumn(index)">
<Plus :size="15"/>
</Button>
</div>
</template>
</div>
</div>
<div class="grid gap-3 md:grid-cols-2 md:gap-3 lg:grid-cols-12 h-[480px] overflow-y-auto pt-2 pb-2">
<template v-for="(item, index) in formState.columns" :key="index">
<div>
<Input v-model="item.name" type="text"/>
</div>
</TabsContent>
<TabsContent value="configure">
<Card class="border-0 mt-5 shadow-transparent">
<CardContent class="grid gap-6 justify-center pt-2 pb-2">
<div class="grid grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="name">{{ $t('common.name') }}</Label>
<Input v-model="formState.name as string"/>
</div>
<div>
<Input v-model="item.aliasName" type="text"/>
</div>
<div>
<Select v-model="item.type">
<div class="grid gap-2">
<Label for="executor">{{ $t('common.executor') }}</Label>
<Select v-model="formState.executor">
<SelectTrigger>
<SelectValue placeholder="Select a fruit"/>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectItem value="STRING">{{ $t('dataset.common.columnTypeString') }}</SelectItem>
<SelectItem value="NUMBER">{{ $t('dataset.common.columnTypeNumber') }}</SelectItem>
<SelectItem value="NUMBER_SIGNED">{{ $t('dataset.common.columnTypeNumberSigned') }}</SelectItem>
<SelectItem value="BOOLEAN">{{ $t('dataset.common.columnTypeBoolean') }}</SelectItem>
<SelectItem value="DATETIME">{{ $t('dataset.common.columnTypeDateTime') }}</SelectItem>
<SelectItem v-for="item in executors" :value="item">{{ item }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="ml-4">
<div class="flex items-center space-x-2 mt-2">
<Label for="airplane-mode">{{ $t('dataset.common.columnModeMetric') }}</Label>
<Switch v-model="item.mode" :default-checked="item.mode === 'DIMENSION'" @update:checked="setMode(item, $event)"/>
<Label for="airplane-mode">{{ $t('dataset.common.columnModeDimension') }}</Label>
</div>
<div class="grid gap-2">
<Label for="syncMode">{{ $t('dataset.common.syncMode') }}</Label>
<Select v-model="formState.syncMode">
<SelectTrigger>
<SelectValue :placeholder="$t('card.tip.roleHolder')"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="MANUAL">{{ $t('dataset.common.syncModeManual') }}</SelectItem>
<SelectItem value="TIMING">{{ $t('dataset.common.syncModeTiming') }}</SelectItem>
<SelectItem value="OUT_SYNC">{{ $t('dataset.common.syncModeOutSync') }}</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Input v-model="item.defaultValue" type="text" :disabled="item.virtualColumn"/>
<div class="grid gap-2" v-if="formState.syncMode === 'TIMING'">
<Label for="syncMode">{{ $t('dataset.common.columnExpression') }}</Label>
<Input v-model="formState.expression as string" placeholder="0 0 * * * ?"/>
</div>
<div class="mt-2">
<Switch v-model="item.nullable" :disabled="item.virtualColumn" :default-checked="item.nullable"
@update:checked="setNullable(item, $event)"/>
<div class="grid gap-2" v-if="formState.syncMode === 'TIMING'">
<Label for="syncMode">{{ $t('common.scheduler') }}</Label>
<Select v-model="formState.scheduler">
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="item in schedulers" :value="item">{{ item }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="mt-2 ml-4">
<Switch v-model="item.orderByKey" :default-checked="item.orderByKey" :disabled="item.virtualColumn"
@update:checked="setOrderByKey(item, $event)"/>
</div>
<div class="mt-2 ml-4">
<Switch v-model="item.partitionKey" :disabled="item.virtualColumn" :default-checked="item.partitionKey"
@update:checked="setPartitionKey(item, $event)"/>
</div>
<div class="mt-2 ml-4">
<Switch v-model="item.primaryKey" :disabled="item.virtualColumn" :default-checked="item.primaryKey"
@update:checked="setPrimaryKey(item, $event)"/>
</div>
<div class="mt-2 ml-4">
<Switch v-model="item.samplingKey" :disabled="item.virtualColumn" :default-checked="item.samplingKey"
@update:checked="setSamplingKey(item, $event)"/>
</div>
<div>
<Input v-model="item.length" type="number" :disabled="item.type === 'BOOLEAN' || item.type === 'DATETIME' || item.virtualColumn"/>
</div>
<div class="space-x-1 ml-4">
<Popover>
<PopoverTrigger as-child>
<Button class="rounded-full w-8 h-8" variant="outline" size="icon">
<Pencil :size="15"/>
</Button>
</PopoverTrigger>
<PopoverContent class="w-80">
<div class="grid gap-4">
<div class="space-y-2">
<h4 class="font-medium leading-none">{{ $t('dataset.common.columnComment') }}</h4>
</div>
<Textarea v-model="item.comment"/>
</div>
</PopoverContent>
</Popover>
<Button class="rounded-full w-8 h-8" variant="destructive" size="icon" :disabled="!item.customColumn" @click="handlerRemoveColumn(index)">
<Trash :size="15"/>
</Button>
<Button class="rounded-full w-8 h-8" size="icon" @click="handlerAddColumn(index)">
<Plus :size="15"/>
</Button>
</div>
</template>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="configure">
<Card class="border-0 mt-5 shadow-transparent">
<CardContent class="grid gap-6 justify-center pt-2 pb-2">
<div class="grid grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="name">{{ $t('common.name') }}</Label>
<Input v-model="formState.name as string"/>
</div>
<div class="grid gap-2">
<Label for="executor">{{ $t('common.executor') }}</Label>
<Select v-model="formState.executor">
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="item in executors" :value="item">{{ item }}</SelectItem>
</SelectContent>
</Select>
<Separator/>
<div class="grid gap-2 -mt-3">
<Label for="description">
<HoverCard>
<HoverCardTrigger as-child>
<Button variant="link" class="-ml-4">{{ $t('dataset.common.dataLifeCycle') }}</Button>
</HoverCardTrigger>
<HoverCardContent class="w-80">
{{ $t('dataset.tip.lifeCycle') }}
</HoverCardContent>
</HoverCard>
</Label>
</div>
<div class="grid gap-2">
<Label for="syncMode">{{ $t('dataset.common.syncMode') }}</Label>
<Select v-model="formState.syncMode">
<SelectTrigger>
<SelectValue :placeholder="$t('card.tip.roleHolder')"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="MANUAL">{{ $t('dataset.common.syncModeManual') }}</SelectItem>
<SelectItem value="TIMING">{{ $t('dataset.common.syncModeTiming') }}</SelectItem>
<SelectItem value="OUT_SYNC">{{ $t('dataset.common.syncModeOutSync') }}</SelectItem>
</SelectContent>
</Select>
<Alert v-if="formState.columns.filter(item => item.type === 'DATETIME').length === 0" variant="destructive" class="-mt-3">
{{ $t('dataset.tip.lifeCycleMustDateColumn') }}
</Alert>
<div v-else class="grid grid-cols-2 gap-4 -mt-3">
<div class="grid gap-2">
<Label>{{ $t('dataset.common.lifeCycleColumn') }}</Label>
<Select v-model="formState.lifeCycleColumn as string">
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="item in formState.columns.filter(item => item.type === 'DATETIME')" :key="item.name" :value="item.name">
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="grid gap-2">
<Label>{{ $t('dataset.common.lifeCycleNumber') }}</Label>
<Input :disabled="!formState.lifeCycleColumn" type="number" v-model="formState.lifeCycle as number"/>
</div>
<div class="grid gap-2">
<Label>{{ $t('dataset.common.lifeCycleNumber') }}</Label>
<Select :disabled="!formState.lifeCycleColumn" v-model="formState.lifeCycleType as string">
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectItem value="MONTH">
{{ $t('dataset.common.lifeCycleMonth') }}
</SelectItem>
<SelectItem value="WEEK">
{{ $t('dataset.common.lifeCycleWeek') }}
</SelectItem>
<SelectItem value="DAY">
{{ $t('dataset.common.lifeCycleDay') }}
</SelectItem>
<SelectItem value="HOUR">
{{ $t('dataset.common.lifeCycleHour') }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div class="grid gap-2" v-if="formState.syncMode === 'TIMING'">
<Label for="syncMode">{{ $t('dataset.common.columnExpression') }}</Label>
<Input v-model="formState.expression as string" placeholder="0 0 * * * ?"/>
<Separator/>
<div class="grid gap-2 -mt-3">
<Label for="description">{{ $t('common.description') }}</Label>
<Textarea v-model="formState.description as string"/>
</div>
<div class="grid gap-2" v-if="formState.syncMode === 'TIMING'">
<Label for="syncMode">{{ $t('common.scheduler') }}</Label>
<Select v-model="formState.scheduler">
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="item in schedulers" :value="item">{{ item }}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Separator/>
<div class="grid gap-2 -mt-3">
<Label for="description">
<HoverCard>
<HoverCardTrigger as-child>
<Button variant="link" class="-ml-4">{{ $t('dataset.common.dataLifeCycle') }}</Button>
</HoverCardTrigger>
<HoverCardContent class="w-80">
{{ $t('dataset.tip.lifeCycle') }}
</HoverCardContent>
</HoverCard>
</Label>
</div>
<Alert v-if="formState.columns.filter(item => item.type === 'DATETIME').length === 0" variant="destructive" class="-mt-3">
{{ $t('dataset.tip.lifeCycleMustDateColumn') }}
</Alert>
<div v-else class="grid grid-cols-2 gap-4 -mt-3">
<div class="grid gap-2">
<Label>{{ $t('dataset.common.lifeCycleColumn') }}</Label>
<Select v-model="formState.lifeCycleColumn as string">
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="item in formState.columns.filter(item => item.type === 'DATETIME')" :key="item.name" :value="item.name">
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="grid gap-2">
<Label>{{ $t('dataset.common.lifeCycleNumber') }}</Label>
<Input :disabled="!formState.lifeCycleColumn" type="number" v-model="formState.lifeCycle as number"/>
</div>
<div class="grid gap-2">
<Label>{{ $t('dataset.common.lifeCycleNumber') }}</Label>
<Select :disabled="!formState.lifeCycleColumn" v-model="formState.lifeCycleType as string">
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectItem value="MONTH">
{{ $t('dataset.common.lifeCycleMonth') }}
</SelectItem>
<SelectItem value="WEEK">
{{ $t('dataset.common.lifeCycleWeek') }}
</SelectItem>
<SelectItem value="DAY">
{{ $t('dataset.common.lifeCycleDay') }}
</SelectItem>
<SelectItem value="HOUR">
{{ $t('dataset.common.lifeCycleHour') }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Separator/>
<div class="grid gap-2 -mt-3">
<Label for="description">{{ $t('common.description') }}</Label>
<Textarea v-model="formState.description as string"/>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</SheetContent>
</Sheet>
</div>
<div v-else class="mt-3 justify-center items-center">
<div class="flex flex-col items-center space-y-3">
<Alert variant="destructive" class="w-1/3">
{{ i18n.t('dataset.common.onlyPreviewCreate') }}
</Alert>
<Button>
<RouterLink to="/admin/query">
{{ i18n.t('dataset.common.returnQuery') }}
</RouterLink>
</Button>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</SheetContent>
</Sheet>
</div>
<div v-else-if="!sourceInfo" class="mt-3 justify-center items-center">
<div class="flex flex-col items-center space-y-3">
<Alert variant="destructive" class="w-1/3">
{{ i18n.t('dataset.common.onlyPreviewCreate') }}
</Alert>
<Button>
<RouterLink to="/admin/query">
{{ i18n.t('dataset.common.returnQuery') }}
</RouterLink>
</Button>
</div>
</div>
</div>
</div>
@ -262,8 +277,7 @@
<script lang="ts">
import { defineComponent } from 'vue'
import { mapState } from 'vuex'
import { Button } from '@/components/ui/button'
import Button from '@/views/ui/button'
import { useI18n } from 'vue-i18n'
import GridOptions from '@/views/components/grid/GridOptions'
import DatasetService from '@/services/dataset'
@ -272,7 +286,7 @@ import { GridColumn } from '@/views/components/grid/GridColumn'
import PluginService from '@/services/plugin'
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Alert } from '@/components/ui/alert';
import { Alert } from '@/components/ui/alert'
import CircularLoading from '@/views/components/loading/CircularLoading.vue'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
@ -281,16 +295,29 @@ import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Pencil, Plus, Trash } from 'lucide-vue-next'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import Card from '@/views/ui/card'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Separator } from '@/components/ui/separator'
import { ToastUtils } from '@/utils/toast'
import { AgGridVue } from 'ag-grid-vue3'
import 'ag-grid-community/styles/ag-grid.css'
import '@/views/components/grid/ag-theme-datacap.css'
import { DatasetModel } from '@/model/dataset'
import { ResponseModel } from '@/model/response.ts'
import AceEditor from '@/views/components/editor/AceEditor.vue'
import { SourceModel } from '@/model/source.ts'
import SourceService from '@/services/source'
import ExecuteService from '@/services/execute'
import { ExecuteModel } from '@/model/execute.ts'
import { FormControl, FormField, FormItem } from '@/components/ui/form'
import { ArrayUtils } from '@/utils/array.ts'
import { join } from 'lodash'
export default defineComponent({
name: 'DatasetInfo',
components: {
FormItem, FormField, FormControl,
AceEditor,
Separator,
Textarea,
Label,
@ -304,13 +331,10 @@ export default defineComponent({
Tabs, TabsContent, TabsList, TabsTrigger,
Popover, PopoverContent, PopoverTrigger,
Pencil, Trash, Plus,
Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,
Card,
HoverCard, HoverCardContent, HoverCardTrigger,
AgGridVue
},
computed: {
...mapState(['data'])
},
setup()
{
const i18n = useI18n()
@ -336,17 +360,21 @@ export default defineComponent({
id: null,
name: null as string | null | undefined,
description: null as string | null | undefined,
query: null,
query: null as string | null,
syncMode: 'MANUAL',
columns: [] as any[],
source: {id: null},
source: { id: null },
expression: null as string | null,
scheduler: 'Default',
executor: 'Default',
lifeCycle: null as number | null,
lifeCycleColumn: null as string | null,
lifeCycleType: null as string | null
}
},
data: null as ResponseModel | null,
sourceInfo: null as SourceModel | null,
value: '',
running: false
}
},
created()
@ -358,70 +386,60 @@ export default defineComponent({
{
setTimeout(() => {
PluginService.getPlugins()
.then(response => {
if (response.status) {
this.schedulers = response.data['scheduler']
this.executors = response.data['executor']
}
})
.then(response => {
if (response.status) {
this.schedulers = response.data['scheduler']
this.executors = response.data['executor']
}
})
const code = this.$route.params.code
const sourceCode = this.$route.params.sourceCode
if (code) {
this.loading = true
this.code = code as string
const axios = new HttpUtils().getAxios()
axios.all([DatasetService.getByCode(this.code), DatasetService.getColumnsByCode(this.code)])
.then(axios.spread((info, column) => {
if (info.status) {
this.formState = info.data
this.formState.source.id = info.data.source.id
}
if (column.status) {
this.formState.columns = column.data
}
}))
.finally(() => this.loading = false)
.then(axios.spread((info, column) => {
if (info.status) {
this.formState = info.data
this.formState.source.id = info.data.source.id
this.sourceInfo = info.data.source
this.value = info.data.query
this.handlerRun()
}
if (column.status) {
this.formState.columns = column.data
}
}))
.finally(() => this.loading = false)
}
else {
this.formState.source.id = this.data?.sourceId
this.formState.query = this.data?.query
this.data?.headers.forEach((header: any, index: number) => {
const columnDef: GridColumn = {headerName: header, field: header}
this.columnDefs.push(columnDef)
const column = {
id: null,
name: `column_${index + 1}`,
aliasName: header.replace('(', '_').replace(')', ''),
type: 'STRING',
comment: header,
defaultValue: null,
position: index,
nullable: false,
length: 0,
original: header,
orderByKey: false,
partitionKey: false,
primaryKey: false,
samplingKey: false,
mode: 'DIMENSION',
virtualColumn: false,
customColumn: false
}
this.formState.columns.push(column)
})
else if (sourceCode) {
this.loading = true
SourceService.getByCode(sourceCode as string)
.then(response => {
if (response.status) {
this.sourceInfo = response.data
this.formState.source.id = response.data.id
}
})
.finally(() => this.loading = false)
}
})
},
handlerCreate()
{
this.saving = true
DatasetService.saveOrUpdate(this.formState as unknown as DatasetModel)
.then(response => {
if (response.status) {
ToastUtils.success(`${this.$t('dataset.create')} [ ${this.formState.name} ] ${this.$t('common.success')}`)
this.$router.push('/admin/dataset')
}
})
.finally(() => this.saving = false)
if (!this.beforeCheck()) {
this.saving = true
this.formState.query = this.value
DatasetService.saveOrUpdate(this.formState as unknown as DatasetModel)
.then(response => {
if (response.status) {
ToastUtils.success(`${ this.$t('dataset.tip.publishSuccess').replace('$VALUE', this.formState.name as string) }`)
this.$router.push('/admin/dataset')
}
})
.finally(() => this.saving = false)
}
},
handlerAddColumn(index: number)
{
@ -449,10 +467,80 @@ export default defineComponent({
{
this.formState.columns.splice(index, 1)
},
handlerRun()
{
const configure: ExecuteModel = {
content: this.value,
name: this.sourceInfo?.id as unknown as string,
mode: 'DATASET',
format: 'JSON'
}
this.running = true
ExecuteService.execute(configure, null)
.then((response) => {
if (response.status) {
this.data = response
response.data?.headers.forEach((header: any, index: number) => {
const columnDef: GridColumn = { headerName: header, field: header }
this.columnDefs.push(columnDef)
if (this.formState.columns.length === 0) {
const column = {
id: null,
name: `column_${ index + 1 }`,
aliasName: header.replace('(', '_').replace(')', ''),
type: 'STRING',
comment: header,
defaultValue: null,
position: index,
nullable: false,
length: 0,
original: header,
orderByKey: false,
partitionKey: false,
primaryKey: false,
samplingKey: false,
mode: 'DIMENSION',
virtualColumn: false,
customColumn: false
}
this.formState.columns.push(column)
}
})
}
else {
ToastUtils.error(response.message)
}
})
.finally(() => this.running = false)
},
beforeCheck(): boolean
{
const duplicateColumns = ArrayUtils.findDuplicates(this.formState.columns)
if (duplicateColumns.length > 0) {
this.validator = true
this.validatorMessage = this.$t('dataset.validator.duplicateColumn').replace('$VALUE', join(duplicateColumns, ','))
return true
}
const orderByColumns = this.formState.columns.filter(item => item.orderByKey)
const primaryKeyColumns = this.formState.columns.filter(item => item.primaryKey)
if (orderByColumns.length === 0 && primaryKeyColumns.length === 0) {
this.validator = true
this.validatorMessage = this.$t('dataset.validator.specifiedColumn')
return true
}
if (!this.formState.name) {
this.validator = true
this.validatorMessage = this.$t('dataset.validator.specifiedName')
return true
}
return false
},
validatorSampling()
{
const samplingColumns = this.formState.columns
.filter((item: { samplingKey: boolean; }) => item.samplingKey)
.filter((item: { samplingKey: boolean; }) => item.samplingKey)
if (samplingColumns.length === 0) {
this.validator = false
this.validatorMessage = null
@ -460,9 +548,9 @@ export default defineComponent({
}
const orderByColumns = this.formState.columns
.filter((item: { orderByKey: boolean; }) => item.orderByKey)
.filter((item: { orderByKey: boolean; }) => item.orderByKey)
const isNameInOrderByColumns = samplingColumns.every((samplingItem: { name: string; }) => {
return orderByColumns.some((orderByItem: { name: string; }) => orderByItem.name === samplingItem.name);
return orderByColumns.some((orderByItem: { name: string; }) => orderByItem.name === samplingItem.name)
})
if (!isNameInOrderByColumns) {
this.validator = true
@ -507,5 +595,5 @@ export default defineComponent({
this.validatorSampling()
}
}
});
})
</script>

View File

@ -16,6 +16,8 @@
<Tag v-else-if="row.mode === 'HISTORY'">{{ $t('common.history') }}</Tag>
<Tag v-else-if="row.mode === 'REPORT'">{{ $t('common.report') }}</Tag>
<Tag v-else-if="row.mode === 'SNIPPET'">{{ $t('common.snippet') }}</Tag>
<Tag v-else-if="row.mode === 'DATASET'">{{ $t('common.dataset') }}</Tag>
<Tag v-else>{{ row.mode }}</Tag>
</template>
<template #state="{ row }">
<Tag :class="row.state === 'SUCCESS' ? '' : 'bg-color-error'">{{ row.state }}</Tag>

View File

@ -177,7 +177,8 @@ export default defineComponent({
selectSource: {
id: null as string | null | undefined,
type: null as string | null | undefined,
engine: null as string | null | undefined
engine: null as string | null | undefined,
code: null as string | null | undefined
},
selectEditor: {
editorMaps: new Map<string, EditorInstance>(),
@ -259,6 +260,7 @@ export default defineComponent({
this.selectSource.id = idAndType[0]
this.selectSource.type = idAndType[1]
this.selectSource.engine = idAndType[1]
this.selectSource.code = idAndType[2]
const instance = this.selectEditor.editorMaps.get(this.selectEditor.activeKey as string)
if (instance) {
this.handlerEditorDidMount(instance.instance as any, idAndType[1])
@ -396,7 +398,8 @@ export default defineComponent({
width: editorContainer.offsetWidth + 20,
showSeriesNumber: false,
sourceId: this.selectSource.id as unknown as number,
query: content
query: content,
code: this.selectSource.code as string
}
this.responseConfigure.gridConfigure = tConfigure
editorInstance.instance?.setValue(response.data.content)

View File

@ -57,17 +57,17 @@ export default defineComponent({
handlerInitialize()
{
this.loading = true
const axios = new HttpUtils().getAxios();
const axios = new HttpUtils().getAxios()
axios.all([UserService.getSourceCount(), UserService.getQueryCount()])
.then(axios.spread((source, query) => {
if (source.status) {
this.summary.sourceCount = source.data
}
if (query.status) {
this.summary.queryCount = query.data
}
}))
.finally(() => this.loading = false)
.then(axios.spread((source, query) => {
if (source.status) {
this.summary.sourceCount = source.data
}
if (query.status) {
this.summary.queryCount = query.data
}
}))
.finally(() => this.loading = false)
}
}
})

View File

@ -14,17 +14,20 @@
<CardContent :class="`${bodyClass}`">
<slot/>
</CardContent>
<CardFooter>
<slot name="footer"/>
</CardFooter>
</Card>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
export default defineComponent({
name: 'DcCard',
components: {
Card, CardContent, CardHeader, CardTitle
CardFooter, Card, CardContent, CardHeader, CardTitle
},
props: {
title: {

View File

@ -0,0 +1,68 @@
<template>
<Carousel :orientation="orientation as any" :plugins="[Autoplay({delay: delay})]">
<CarouselContent>
<CarouselItem v-for="item in items">
<div v-if="item.isAlert">
<DcLink :external="item.external" :link="item.link">
<Alert :title="item.title"/>
</DcLink>
</div>
<div v-else>
<DcLink :external="item.external" :link="item.link"/>
</div>
</CarouselItem>
</CarouselContent>
<CarouselPrevious v-if="previous"/>
<CarouselNext v-if="next"/>
</Carousel>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel'
import Autoplay from 'embla-carousel-autoplay'
import { CarouselModel } from './model.ts'
import Alert from '@/views/ui/alert'
import DcLink from '@/views/components/link/DcLink.vue'
export default defineComponent({
name: 'DcCarousel',
computed: {
Autoplay()
{
return Autoplay
}
},
components: {
DcLink,
Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,
Alert
},
props: {
autoPlay: {
type: Boolean,
default: true
},
delay: {
type: Number,
default: 2000
},
previous: {
type: Boolean,
default: false
},
next: {
type: Boolean,
default: false
},
orientation: {
type: String,
default: 'horizontal'
},
items: {
type: Array as () => CarouselModel[],
default: () => new Array<CarouselModel>()
}
}
})
</script>

View File

@ -0,0 +1,3 @@
import Carousel from '@/views/ui/carousel/carousel.vue'
export default Carousel

View File

@ -0,0 +1,9 @@
export interface CarouselModel
{
title?: string
description?: string
image?: string
link?: string
external?: boolean
isAlert?: boolean
}

View File

@ -1,10 +1,10 @@
<template>
<div class="flex items-center space-x-4 text-sm flex-row w-full overflow-hidden">
<Separator :class="cn(orientation === Position.left && 'w-10')"/>
<Separator :class="cn(position === Position.left && 'w-10')" :orientation="orientation as any"/>
<div class="flex items-center flex-grow">
<slot name="content"/>
</div>
<Separator :class="cn(orientation === Position.right && 'w-10')"/>
<Separator :class="cn(position === Position.right && 'w-10')" :orientation="orientation as any"/>
</div>
</template>
@ -23,6 +23,9 @@ export default defineComponent({
name: 'DcDivider',
components: { Separator },
props: {
position: {
type: String
},
orientation: {
type: String
}

View File

@ -22,6 +22,7 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"allowSyntheticDefaultImports": true,
"paths": {
"@/*": [
"./src/*"

View File

@ -1,4 +0,0 @@
> 1%
last 2 versions
not dead
not ie 11

View File

@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -1,22 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-array-constructor": "off",
"vue/no-mutating-props": "off",
'@typescript-eslint/no-this-alias': 'off'
}
}

View File

@ -1,23 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@ -1,71 +0,0 @@
{
"name": "datacap-console",
"description": "DataCap console",
"version": "2024.3.1-SNAPSHOT",
"private": true,
"scripts": {
"dev": "vue-cli-service serve",
"report": "vue-cli-service build --report",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@antv/x6": "^2.16.1",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.0-5",
"@kangc/v-md-editor": "^2.3.15",
"@types/nprogress": "^0.2.0",
"@visactor/vchart": "^1.8.6",
"@visactor/vtable": "^0.17.8",
"@vue-flow/background": "^1.2.0",
"@vue-flow/controls": "^1.1.0",
"@vue-flow/core": "^1.27.1",
"@vue-flow/node-resizer": "^1.3.6",
"ace-builds": "^1.30.0",
"ag-grid-community": "^29.3.5",
"ag-grid-vue3": "^29.3.5",
"ansi_up": "^6.0.2",
"axios": "^0.27.2",
"echarts": "^5.4.0",
"export-to-csv": "^0.2.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nprogress": "^0.2.0",
"prismjs": "^1.29.0",
"tippy.js": "^6.3.7",
"uuid": "^9.0.1",
"view-ui-plus": "^1.3.1",
"vue": "^3.2.13",
"vue-clipboard3": "^2.0.0",
"vue-i18n": "^9.2.2",
"vue-router": "^4.0.3",
"vue3-ace-editor": "^2.2.3",
"vue3-calendar-heatmap": "^2.0.5",
"vue3-grid-layout-next": "^1.0.6",
"vue3-markdown": "^1.1.7",
"vuedraggable": "^4.1.0",
"vuex": "^4.1.0",
"watermark-dom": "^2.3.0",
"webpack-bundle-analyzer": "^4.6.1"
},
"devDependencies": {
"@types/lodash": "^4.14.189",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-standard": "^6.1.0",
"@vue/eslint-config-typescript": "^9.1.0",
"core-js": "^3.8.3",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^8.0.3",
"typescript": "~4.5.5"
}
}

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>static/images/logo.png">
<!-- <title><%= htmlWebpackPlugin.options.title %></title>-->
<title>DataCap</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Some files were not shown because too many files have changed in this diff Show More