diff --git a/wren-ui/src/apollo/client/graphql/__types__.ts b/wren-ui/src/apollo/client/graphql/__types__.ts index d50d4275bc..328320a1bf 100644 --- a/wren-ui/src/apollo/client/graphql/__types__.ts +++ b/wren-ui/src/apollo/client/graphql/__types__.ts @@ -40,6 +40,48 @@ export type AdjustmentTask = { traceId?: Maybe; }; +export type ApiHistoryFilterInput = { + apiType?: InputMaybe; + endDate?: InputMaybe; + projectId?: InputMaybe; + startDate?: InputMaybe; + statusCode?: InputMaybe; + threadId?: InputMaybe; +}; + +export type ApiHistoryPaginatedResponse = { + __typename?: 'ApiHistoryPaginatedResponse'; + hasMore: Scalars['Boolean']; + items: Array; + total: Scalars['Int']; +}; + +export type ApiHistoryPaginationInput = { + limit: Scalars['Int']; + offset: Scalars['Int']; +}; + +export type ApiHistoryResponse = { + __typename?: 'ApiHistoryResponse'; + apiType: ApiType; + createdAt: Scalars['String']; + durationMs?: Maybe; + headers?: Maybe; + id: Scalars['String']; + projectId: Scalars['Int']; + requestPayload?: Maybe; + responsePayload?: Maybe; + statusCode?: Maybe; + threadId?: Maybe; + updatedAt: Scalars['String']; +}; + +export enum ApiType { + GENERATE_SQL = 'GENERATE_SQL', + GENERATE_VEGA_SPEC = 'GENERATE_VEGA_SPEC', + RUN_SQL = 'RUN_SQL' +} + export type AskingTask = { __typename?: 'AskingTask'; candidates: Array; @@ -1012,6 +1054,7 @@ export enum ProjectLanguage { export type Query = { __typename?: 'Query'; adjustmentTask?: Maybe; + apiHistory: ApiHistoryPaginatedResponse; askingTask?: Maybe; autoGenerateRelation: Array; dashboardItems: Array; @@ -1045,6 +1088,12 @@ export type QueryAdjustmentTaskArgs = { }; +export type QueryApiHistoryArgs = { + filter?: InputMaybe; + pagination?: InputMaybe; +}; + + export type QueryAskingTaskArgs = { taskId: Scalars['String']; }; diff --git a/wren-ui/src/apollo/client/graphql/apiManagement.generated.ts b/wren-ui/src/apollo/client/graphql/apiManagement.generated.ts new file mode 100644 index 0000000000..c112c74cfb --- /dev/null +++ b/wren-ui/src/apollo/client/graphql/apiManagement.generated.ts @@ -0,0 +1,64 @@ +import * as Types from './__types__'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type ApiHistoryQueryVariables = Types.Exact<{ + filter?: Types.InputMaybe; + pagination: Types.ApiHistoryPaginationInput; +}>; + + +export type ApiHistoryQuery = { __typename?: 'Query', apiHistory: { __typename?: 'ApiHistoryPaginatedResponse', total: number, hasMore: boolean, items: Array<{ __typename?: 'ApiHistoryResponse', id: string, projectId: number, apiType: Types.ApiType, threadId?: string | null, headers?: any | null, requestPayload?: any | null, responsePayload?: any | null, statusCode?: number | null, durationMs?: number | null, createdAt: string, updatedAt: string }> } }; + + +export const ApiHistoryDocument = gql` + query ApiHistory($filter: ApiHistoryFilterInput, $pagination: ApiHistoryPaginationInput!) { + apiHistory(filter: $filter, pagination: $pagination) { + items { + id + projectId + apiType + threadId + headers + requestPayload + responsePayload + statusCode + durationMs + createdAt + updatedAt + } + total + hasMore + } +} + `; + +/** + * __useApiHistoryQuery__ + * + * To run a query within a React component, call `useApiHistoryQuery` and pass it any options that fit your needs. + * When your component renders, `useApiHistoryQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useApiHistoryQuery({ + * variables: { + * filter: // value for 'filter' + * pagination: // value for 'pagination' + * }, + * }); + */ +export function useApiHistoryQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ApiHistoryDocument, options); + } +export function useApiHistoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ApiHistoryDocument, options); + } +export type ApiHistoryQueryHookResult = ReturnType; +export type ApiHistoryLazyQueryHookResult = ReturnType; +export type ApiHistoryQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/wren-ui/src/apollo/client/graphql/apiManagement.ts b/wren-ui/src/apollo/client/graphql/apiManagement.ts new file mode 100644 index 0000000000..4a7680f0bf --- /dev/null +++ b/wren-ui/src/apollo/client/graphql/apiManagement.ts @@ -0,0 +1,26 @@ +import { gql } from '@apollo/client'; + +export const API_HISTORY = gql` + query ApiHistory( + $filter: ApiHistoryFilterInput + $pagination: ApiHistoryPaginationInput! + ) { + apiHistory(filter: $filter, pagination: $pagination) { + items { + id + projectId + apiType + threadId + headers + requestPayload + responsePayload + statusCode + durationMs + createdAt + updatedAt + } + total + hasMore + } + } +`; diff --git a/wren-ui/src/components/HeaderBar.tsx b/wren-ui/src/components/HeaderBar.tsx index 51c5591e8f..a3a0292c49 100644 --- a/wren-ui/src/components/HeaderBar.tsx +++ b/wren-ui/src/components/HeaderBar.tsx @@ -71,6 +71,14 @@ export default function HeaderBar() { > Knowledge + router.push(Path.APIManagementHistory)} + > + API + )} diff --git a/wren-ui/src/components/code/BaseCodeBlock.tsx b/wren-ui/src/components/code/BaseCodeBlock.tsx new file mode 100644 index 0000000000..123df81737 --- /dev/null +++ b/wren-ui/src/components/code/BaseCodeBlock.tsx @@ -0,0 +1,201 @@ +import { useEffect } from 'react'; +import styled from 'styled-components'; +import { Button, Typography } from 'antd'; +import CheckOutlined from '@ant-design/icons/CheckOutlined'; +import CopyOutlined from '@ant-design/icons/CopyOutlined'; +import { Loading } from '@/components/PageLoading'; +import '@/components/editor/AceEditor'; + +export interface BaseProps { + code: string; + copyable?: boolean; + inline?: boolean; + loading?: boolean; + maxHeight?: string; + showLineNumbers?: boolean; + backgroundColor?: string; +} + +const getBlockStyles = (props: { + inline?: boolean; + backgroundColor?: string; +}) => { + if (props.inline) { + return ` + display: inline; + border: none; + background: transparent !important; + padding: 0; + * { display: inline !important; } + `; + } + return ` + background: ${props.backgroundColor || 'var(--gray-1)'} !important; + padding: 8px; + `; +}; + +export const Block = styled.div<{ + maxHeight?: string; + inline?: boolean; + backgroundColor?: string; +}>` + position: relative; + white-space: pre; + font-size: 13px; + border: 1px var(--gray-4) solid; + border-radius: 4px; + font-family: 'Source Code Pro', monospace; + user-select: text; + cursor: text; + &:focus { + outline: none; + } + ${getBlockStyles} + + .adm-code-wrap { + ${(props) => (props.inline ? '' : 'overflow: auto;')} + ${(props) => (props.maxHeight ? `max-height: ${props.maxHeight}px;` : ``)} + user-select: text; + } + + .adm-code-line { + display: block; + user-select: text; + &-number { + user-select: none; + display: inline-block; + min-width: 14px; + text-align: right; + margin-right: 1em; + color: var(--gray-6); + font-weight: 700; + font-size: 12px; + } + } +`; + +export const CopyText = styled(Typography.Text)` + position: absolute; + top: 0; + right: 0; + font-size: 0; + button { + background: var(--gray-1) !important; + } + + .ant-typography-copy { + font-size: 12px; + } + + .ant-btn:not(:hover) { + color: var(--gray-8); + } +`; + +export const addThemeStyleManually = (cssText: string) => { + const id = 'ace-tomorrow'; + const themeElement = document.getElementById(id); + if (!themeElement) { + const styleElement = document.createElement('style'); + styleElement.id = id; + document.head.appendChild(styleElement); + styleElement.appendChild(document.createTextNode(cssText)); + } +}; + +export const createCodeBlock = (HighlightRules: any) => { + return function CodeBlock(props: BaseProps) { + const { + code, + copyable, + maxHeight, + inline, + loading, + showLineNumbers, + backgroundColor, + } = props; + const { ace } = window as any; + const { Tokenizer } = ace.require('ace/tokenizer'); + const rules = new HighlightRules(); + const tokenizer = new Tokenizer(rules.getRules()); + + useEffect(() => { + const { cssText } = ace.require('ace/theme/tomorrow'); + addThemeStyleManually(cssText); + }, []); + + const lines = (code || '').split('\n').map((line, index) => { + const tokens = tokenizer.getLineTokens(line).tokens; + const children = tokens.map((token, index) => { + const classNames = token.type.split('.').map((name) => `ace_${name}`); + return ( + + {token.value} + + ); + }); + + return ( + + {showLineNumbers && ( + {index + 1} + )} + {children} + + ); + }); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'a') { + e.preventDefault(); + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents( + e.currentTarget.querySelector('.adm-code-wrap') || e.currentTarget, + ); + selection?.removeAllRanges(); + selection?.addRange(range); + } + }; + + return ( + + +
+ {lines} + {copyable && ( + } + size="small" + style={{ backgroundColor: 'transparent' }} + />, +
+
+
+ ); + }; +}; diff --git a/wren-ui/src/components/code/JsonCodeBlock.tsx b/wren-ui/src/components/code/JsonCodeBlock.tsx new file mode 100644 index 0000000000..cb58e8f9af --- /dev/null +++ b/wren-ui/src/components/code/JsonCodeBlock.tsx @@ -0,0 +1,14 @@ +import { createCodeBlock, BaseProps } from './BaseCodeBlock'; + +const JsonCodeBlock = (props: BaseProps) => { + const { code, ...rest } = props; + const { ace } = window as any; + const formattedJson = + typeof code === 'string' ? code : JSON.stringify(code, null, 2); + + const { JsonHighlightRules } = ace.require('ace/mode/json_highlight_rules'); + const BaseCodeBlock = createCodeBlock(JsonHighlightRules); + return ; +}; + +export default JsonCodeBlock; diff --git a/wren-ui/src/components/code/SQLCodeBlock.tsx b/wren-ui/src/components/code/SQLCodeBlock.tsx new file mode 100644 index 0000000000..04acf13bf5 --- /dev/null +++ b/wren-ui/src/components/code/SQLCodeBlock.tsx @@ -0,0 +1,10 @@ +import { createCodeBlock, BaseProps } from './BaseCodeBlock'; + +const SQLCodeBlock = (props: BaseProps) => { + const { ace } = window as any; + const { SqlHighlightRules } = ace.require('ace/mode/sql_highlight_rules'); + const BaseCodeBlock = createCodeBlock(SqlHighlightRules); + return ; +}; + +export default SQLCodeBlock; diff --git a/wren-ui/src/components/editor/AceEditor.tsx b/wren-ui/src/components/editor/AceEditor.tsx index 6899b53dbe..0ab8d8ffda 100644 --- a/wren-ui/src/components/editor/AceEditor.tsx +++ b/wren-ui/src/components/editor/AceEditor.tsx @@ -1,6 +1,7 @@ import AceEditor from 'react-ace'; import 'ace-builds/src-noconflict/mode-sql'; +import 'ace-builds/src-noconflict/mode-json'; import 'ace-builds/src-noconflict/theme-tomorrow'; import 'ace-builds/src-noconflict/ext-language_tools'; diff --git a/wren-ui/src/components/editor/CodeBlock.tsx b/wren-ui/src/components/editor/CodeBlock.tsx deleted file mode 100644 index 251f4a1b38..0000000000 --- a/wren-ui/src/components/editor/CodeBlock.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useEffect } from 'react'; -import { Button, Typography } from 'antd'; -import styled from 'styled-components'; -import '@/components/editor/AceEditor'; -import { Loading } from '@/components/PageLoading'; -import CheckOutlined from '@ant-design/icons/CheckOutlined'; -import CopyOutlined from '@ant-design/icons/CopyOutlined'; - -const Block = styled.div<{ inline?: boolean; maxHeight?: string }>` - position: relative; - white-space: pre; - font-size: 14px; - border: 1px var(--gray-4) solid; - border-radius: 4px; - ${(props) => - props.inline - ? ` - display: inline; border: none; background: transparent !important; padding: 0; - * { display: inline !important; } - ` - : `background: var(--gray-1); padding: 8px;`} - - .adm-code-wrap { - ${(props) => (props.inline ? '' : 'overflow: auto;')} - ${(props) => (props.maxHeight ? `max-height: ${props.maxHeight}px;` : ``)} - } - - .adm-code-line { - display: block; - &-number { - user-select: none; - display: inline-block; - min-width: 14px; - text-align: right; - margin-right: 1em; - color: var(--gray-6); - font-weight: 700; - font-size: 14px; - } - } -`; - -const CopyText = styled(Typography.Text)` - position: absolute; - top: 8px; - right: 20px; - font-size: 0; - .ant-typography-copy { - font-size: 12px; - } - - .ant-btn:not(:hover) { - color: var(--gray-8); - } -`; - -interface Props { - code: string; - copyable?: boolean; - inline?: boolean; - loading?: boolean; - maxHeight?: string; - showLineNumbers?: boolean; -} - -const addThemeStyleManually = (cssText) => { - // same id as ace editor appended, it will exist only one. - const id = 'ace-tomorrow'; - const themeElement = document.getElementById(id); - if (!themeElement) { - const styleElement = document.createElement('style'); - styleElement.id = id; - document.head.appendChild(styleElement); - styleElement.appendChild(document.createTextNode(cssText)); - } -}; - -export default function CodeBlock(props: Props) { - const { code, copyable, maxHeight, inline, loading, showLineNumbers } = props; - const { ace } = window as any; - const { Tokenizer } = ace.require('ace/tokenizer'); - const { SqlHighlightRules } = ace.require(`ace/mode/sql_highlight_rules`); - const rules = new SqlHighlightRules(); - const tokenizer = new Tokenizer(rules.getRules()); - - useEffect(() => { - const { cssText } = ace.require('ace/theme/tomorrow'); - addThemeStyleManually(cssText); - }, []); - - const lines = (code || '').split('\n').map((line, index) => { - const tokens = tokenizer.getLineTokens(line).tokens; - const children = tokens.map((token, index) => { - const classNames = token.type.split('.').map((name) => `ace_${name}`); - return ( - - {token.value} - - ); - }); - - return ( - - {showLineNumbers && ( - {index + 1} - )} - {children} - - ); - }); - - return ( - - -
- {lines} - {copyable && ( - } - size="small" - style={{ backgroundColor: 'transparent' }} - />, -
-
-
- ); -} diff --git a/wren-ui/src/components/layouts/PageLayout.tsx b/wren-ui/src/components/layouts/PageLayout.tsx new file mode 100644 index 0000000000..d21ad20d45 --- /dev/null +++ b/wren-ui/src/components/layouts/PageLayout.tsx @@ -0,0 +1,26 @@ +import { Typography } from 'antd'; + +interface PageLayoutProps { + title: string | React.ReactNode; + description?: string | React.ReactNode; + children: React.ReactNode; + titleExtra?: string | React.ReactNode; +} + +export default function PageLayout(props: PageLayoutProps) { + const { title, titleExtra, description, children } = props; + return ( +
+
+ + {title} + + {titleExtra} +
+ {description && ( + {description} + )} +
{children}
+
+ ); +} diff --git a/wren-ui/src/components/modals/SaveAsViewModal.tsx b/wren-ui/src/components/modals/SaveAsViewModal.tsx index 42bd06955d..2b8959ca81 100644 --- a/wren-ui/src/components/modals/SaveAsViewModal.tsx +++ b/wren-ui/src/components/modals/SaveAsViewModal.tsx @@ -2,7 +2,7 @@ import { Button, Form, Input, Modal, Typography } from 'antd'; import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined'; import { ModalAction } from '@/hooks/useModalAction'; import { createViewNameValidator } from '@/utils/validator'; -import CodeBlock from '@/components/editor/CodeBlock'; +import SQLCodeBlock from '@/components/code/SQLCodeBlock'; import { useValidateViewMutation } from '@/apollo/client/graphql/view.generated'; const { Text } = Typography; @@ -78,7 +78,7 @@ export default function SaveAsViewModal(props: Props) { - + diff --git a/wren-ui/src/components/pages/apiManagement/DetailsDrawer.tsx b/wren-ui/src/components/pages/apiManagement/DetailsDrawer.tsx new file mode 100644 index 0000000000..f4984d3b4c --- /dev/null +++ b/wren-ui/src/components/pages/apiManagement/DetailsDrawer.tsx @@ -0,0 +1,126 @@ +import { Drawer, Typography, Row, Col, Tag } from 'antd'; +import { getAbsoluteTime } from '@/utils/time'; +import { DrawerAction } from '@/hooks/useDrawerAction'; +import CheckCircleOutlined from '@ant-design/icons/CheckCircleOutlined'; +import CloseCircleOutlined from '@ant-design/icons/CloseCircleOutlined'; +import JsonCodeBlock from '@/components/code/JsonCodeBlock'; +import { ApiHistoryResponse } from '@/apollo/client/graphql/__types__'; + +type Props = DrawerAction & { + loading?: boolean; +}; + +export default function DetailsDrawer(props: Props) { + const { visible, onClose, defaultValue } = props; + + const { + threadId, + apiType, + createdAt, + durationMs, + statusCode, + headers, + requestPayload, + responsePayload, + } = defaultValue || {}; + + const getStatusTag = (status: number) => { + const isSuccess = status >= 200 && status < 300; + return ( + : } + color={isSuccess ? 'success' : 'error'} + > + {status} + + ); + }; + + return ( + + + + + API type + +
+ {apiType?.toLowerCase()} +
+ + + + Thread ID + +
{threadId || '-'}
+ +
+ + + + Created at + +
{getAbsoluteTime(createdAt)}
+ + + + Duration + +
{durationMs} ms
+ +
+ + + + Status code + +
{getStatusTag(statusCode)}
+ +
+ +
+ + Headers + + +
+ +
+ + Request payload + + +
+ +
+ + Response payload + + +
+
+ ); +} diff --git a/wren-ui/src/components/pages/home/promptThread/ViewSQLTabContent.tsx b/wren-ui/src/components/pages/home/promptThread/ViewSQLTabContent.tsx index fad497068e..811cd81725 100644 --- a/wren-ui/src/components/pages/home/promptThread/ViewSQLTabContent.tsx +++ b/wren-ui/src/components/pages/home/promptThread/ViewSQLTabContent.tsx @@ -15,7 +15,7 @@ import usePromptThreadStore from '@/components/pages/home/promptThread/store'; import PreviewData from '@/components/dataPreview/PreviewData'; import { usePreviewDataMutation } from '@/apollo/client/graphql/home.generated'; -const CodeBlock = dynamic(() => import('@/components/editor/CodeBlock'), { +const SQLCodeBlock = dynamic(() => import('@/components/code/SQLCodeBlock'), { ssr: false, }); @@ -126,7 +126,7 @@ export default function ViewSQLTabContent(props: AnswerResultProps) { - ; @@ -24,7 +24,7 @@ export default function SQLPairDrawer(props: Props) {
SQL statement - SQL statement - +
diff --git a/wren-ui/src/components/sidebar/APIManagement.tsx b/wren-ui/src/components/sidebar/APIManagement.tsx new file mode 100644 index 0000000000..517000d589 --- /dev/null +++ b/wren-ui/src/components/sidebar/APIManagement.tsx @@ -0,0 +1,68 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import styled from 'styled-components'; +import { Path, MENU_KEY } from '@/utils/enum'; +import { OpenInNewIcon } from '@/utils/icons'; +import ApiOutlined from '@ant-design/icons/ApiOutlined'; +import ReadOutlined from '@ant-design/icons/ReadOutlined'; +import SidebarMenu from '@/components/sidebar/SidebarMenu'; + +const Layout = styled.div` + padding: 16px 0; + position: absolute; + z-index: 1; + left: 0; + top: 0; + width: 100%; + background-color: var(--gray-2); + overflow: hidden; +`; + +const MENU_KEY_MAP = { + [Path.APIManagementHistory]: MENU_KEY.API_HISTORY, +}; + +const linkStyle = { color: 'inherit', transition: 'none' }; + +export default function APIManagement() { + const router = useRouter(); + + const menuItems = [ + { + 'data-guideid': 'api-history', + label: ( + + API history + + ), + icon: , + key: MENU_KEY.API_HISTORY, + className: 'pl-4', + }, + { + label: ( + + API reference + + + ), + icon: , + key: MENU_KEY.API_REFERENCE, + className: 'pl-4', + }, + ]; + + return ( + + + + ); +} diff --git a/wren-ui/src/components/sidebar/Knowledge.tsx b/wren-ui/src/components/sidebar/Knowledge.tsx index 1781a356fc..ed29619404 100644 --- a/wren-ui/src/components/sidebar/Knowledge.tsx +++ b/wren-ui/src/components/sidebar/Knowledge.tsx @@ -2,10 +2,9 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; import styled from 'styled-components'; import FunctionOutlined from '@ant-design/icons/FunctionOutlined'; -import { Path, KNOWLEDGE } from '@/utils/enum'; +import { Path, MENU_KEY } from '@/utils/enum'; import { InstructionsSVG } from '@/utils/svgs'; import SidebarMenu from '@/components/sidebar/SidebarMenu'; -import { MENU_KEY_MAP } from '@/components/pages/knowledge/utils'; const Layout = styled.div` padding: 16px 0; @@ -18,6 +17,11 @@ const Layout = styled.div` overflow: hidden; `; +const MENU_KEY_MAP = { + [Path.KnowledgeQuestionSQLPairs]: MENU_KEY.QUESTION_SQL_PAIRS, + [Path.KnowledgeInstructions]: MENU_KEY.INSTRUCTIONS, +}; + const linkStyle = { color: 'inherit', transition: 'none' }; export default function Knowledge() { @@ -32,7 +36,7 @@ export default function Knowledge() { ), icon: , - key: KNOWLEDGE.QUESTION_SQL_PAIRS, + key: MENU_KEY.QUESTION_SQL_PAIRS, className: 'pl-4', }, { @@ -43,7 +47,7 @@ export default function Knowledge() { ), icon: , - key: KNOWLEDGE.INSTRUCTIONS, + key: MENU_KEY.INSTRUCTIONS, className: 'pl-4', }, ]; diff --git a/wren-ui/src/components/sidebar/index.tsx b/wren-ui/src/components/sidebar/index.tsx index 136c848968..442c7255ed 100644 --- a/wren-ui/src/components/sidebar/index.tsx +++ b/wren-ui/src/components/sidebar/index.tsx @@ -8,6 +8,7 @@ import SettingOutlined from '@ant-design/icons/SettingOutlined'; import Home, { Props as HomeSidebarProps } from './Home'; import Modeling, { Props as ModelingSidebarProps } from './Modeling'; import Knowledge from './Knowledge'; +import APIManagement from './APIManagement'; import LearningSection from '@/components/learning'; const Layout = styled.div` @@ -63,6 +64,10 @@ const DynamicSidebar = ( return ; } + if (pathname.startsWith(Path.APIManagement)) { + return ; + } + return null; }; diff --git a/wren-ui/src/components/table/BaseTable.tsx b/wren-ui/src/components/table/BaseTable.tsx index 126cdd0e58..f249ff1294 100644 --- a/wren-ui/src/components/table/BaseTable.tsx +++ b/wren-ui/src/components/table/BaseTable.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { Table, TableProps, Row, Col } from 'antd'; import EllipsisWrapper from '@/components/EllipsisWrapper'; -import CodeBlock from '@/components/editor/CodeBlock'; +import SQLCodeBlock from '@/components/code/SQLCodeBlock'; import { getColumnTypeIcon } from '@/utils/columnType'; import { ComposeDiagramField, getJoinTypeText } from '@/utils/data'; import { makeIterable } from '@/utils/iteration'; @@ -40,7 +40,7 @@ export const COLUMN = { render: (expression) => { return ( - + ); }, diff --git a/wren-ui/src/pages/api-management/history.tsx b/wren-ui/src/pages/api-management/history.tsx new file mode 100644 index 0000000000..67b494aae2 --- /dev/null +++ b/wren-ui/src/pages/api-management/history.tsx @@ -0,0 +1,208 @@ +import Link from 'next/link'; +import { useState } from 'react'; +import { Table, TableColumnsType, Button, Tag, Typography } from 'antd'; +import { getAbsoluteTime } from '@/utils/time'; +import useDrawerAction from '@/hooks/useDrawerAction'; +import { getColumnSearchProps } from '@/utils/table'; +import SiderLayout from '@/components/layouts/SiderLayout'; +import PageLayout from '@/components/layouts/PageLayout'; +import ApiOutlined from '@ant-design/icons/ApiOutlined'; +import EyeOutlined from '@ant-design/icons/EyeOutlined'; +import CheckCircleOutlined from '@ant-design/icons/CheckCircleOutlined'; +import CloseCircleOutlined from '@ant-design/icons/CloseCircleOutlined'; +import SQLCodeBlock from '@/components/code/SQLCodeBlock'; +import DetailsDrawer from '@/components/pages/apiManagement/DetailsDrawer'; +import { useApiHistoryQuery } from '@/apollo/client/graphql/apiManagement.generated'; +import { ApiType, ApiHistoryResponse } from '@/apollo/client/graphql/__types__'; + +const PAGE_SIZE = 10; + +export default function APIHistory() { + const detailsDrawer = useDrawerAction(); + const [currentPage, setCurrentPage] = useState(1); + const [filters, setFilters] = useState>({}); + + const { data, loading } = useApiHistoryQuery({ + fetchPolicy: 'cache-and-network', + variables: { + pagination: { + offset: (currentPage - 1) * PAGE_SIZE, + limit: PAGE_SIZE, + }, + filter: { + apiType: filters['apiType']?.[0], + statusCode: filters['statusCode']?.[0], + threadId: filters['threadId']?.[0], + }, + }, + onError: (error) => console.error(error), + }); + + const columns: TableColumnsType = [ + { + title: 'Timestamp', + dataIndex: 'createdAt', + key: 'createdAt', + width: 180, + render: (timestamp: string) => ( +
{getAbsoluteTime(timestamp)}
+ ), + }, + { + title: 'API type', + dataIndex: 'apiType', + key: 'apiType', + width: 130, + render: (type: ApiHistoryResponse['apiType']) => ( + {type.toLowerCase()} + ), + filters: Object.keys(ApiType).map((type) => ({ + text: type.toLowerCase(), + value: type, + })), + filteredValue: filters['apiType'], + filterMultiple: false, + }, + { + title: 'Status', + dataIndex: 'statusCode', + key: 'statusCode', + width: 80, + render: (status: number) => { + const icon = + status === 200 ? : ; + const color = status === 200 ? 'success' : 'error'; + return ( + + {status} + + ); + }, + filters: [ + { text: 'Successful (code: 2xx)', value: 200 }, + { text: 'Client error (code: 4xx)', value: 400 }, + { text: 'Server error (code: 5xx)', value: 500 }, + ], + filteredValue: filters['statusCode'], + filterMultiple: false, + }, + { + title: 'Question / SQL', + dataIndex: 'requestPayload', + key: 'requestPayload', + render: (payload: Record, record: ApiHistoryResponse) => { + if (record.apiType === ApiType.RUN_SQL && payload.sql) { + return ( +
+ +
+ ); + } + return ( +
{payload.question || payload.sql || '-'}
+ ); + }, + }, + { + title: 'Thread ID', + dataIndex: 'threadId', + key: 'threadId', + width: 200, + render: (threadId: string) => { + if (!threadId) return
-
; + return ( + + {threadId} + + ); + }, + ...getColumnSearchProps({ + dataIndex: 'threadId', + placeholder: 'thread ID', + filteredValue: filters['threadId'], + }), + }, + { + title: 'Duration (ms)', + dataIndex: 'durationMs', + key: 'durationMs', + width: 124, + render: (durationMs: number) => ( +
{durationMs || '-'}
+ ), + }, + { + title: 'Actions', + key: 'actions', + width: 110, + align: 'center', + fixed: 'right', + render: (record) => ( + + ), + }, + ]; + + return ( + + + + API history + + } + description={ + <> +
+ Here you can view the full history of API calls, including request + inputs, responses, and execution details.{' '} + + Learn more. + +
+ + } + > + { + setCurrentPage(pagination.current); + setFilters(filters); + }} + /> + + + + ); +} diff --git a/wren-ui/src/pages/knowledge/instructions.tsx b/wren-ui/src/pages/knowledge/instructions.tsx index 3c8be33cd1..69fc2e90fc 100644 --- a/wren-ui/src/pages/knowledge/instructions.tsx +++ b/wren-ui/src/pages/knowledge/instructions.tsx @@ -9,6 +9,7 @@ import { } from 'antd'; import styled from 'styled-components'; import SiderLayout from '@/components/layouts/SiderLayout'; +import PageLayout from '@/components/layouts/PageLayout'; import { InstructionsSVG } from '@/utils/svgs'; import QuestionOutlined from '@ant-design/icons/QuestionOutlined'; import { MORE_ACTION } from '@/utils/enum'; @@ -28,7 +29,7 @@ import { useDeleteInstructionMutation, } from '@/apollo/client/graphql/instructions.generated'; -const { Paragraph, Title, Text } = Typography; +const { Paragraph, Text } = Typography; const StyledQuestionsBlock = styled.div` margin: -2px -4px; @@ -166,39 +167,40 @@ export default function ManageInstructions() { return ( -
-
- + <PageLayout + title={ + <> <StyledInstructionsIcon className="mr-2 gray-8" /> Manage instruction - - -
- - On this page, you can manage saved instructions that guide Wren AI in - generating SQL queries. These instructions help Wren AI understand - your data model and business rules, improving query accuracy and - reducing the need for manual refinements.{' '} - - Learn more. - - + } + description={ + <> + On this page, you can manage saved instructions that guide Wren AI + in generating SQL queries. These instructions help Wren AI + understand your data model and business rules, improving query + accuracy and reducing the need for manual refinements.{' '} + + Learn more. + + + } + >
- + ); } diff --git a/wren-ui/src/pages/knowledge/question-sql-pairs.tsx b/wren-ui/src/pages/knowledge/question-sql-pairs.tsx index 6962e05f30..7efc246c39 100644 --- a/wren-ui/src/pages/knowledge/question-sql-pairs.tsx +++ b/wren-ui/src/pages/knowledge/question-sql-pairs.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { Button, message, Table, TableColumnsType, Typography } from 'antd'; import { format } from 'sql-formatter'; import SiderLayout from '@/components/layouts/SiderLayout'; +import PageLayout from '@/components/layouts/PageLayout'; import FunctionOutlined from '@ant-design/icons/FunctionOutlined'; import { MORE_ACTION } from '@/utils/enum'; import { getCompactTime } from '@/utils/time'; @@ -20,11 +21,11 @@ import { useDeleteSqlPairMutation, } from '@/apollo/client/graphql/sqlPairs.generated'; -const CodeBlock = dynamic(() => import('@/components/editor/CodeBlock'), { +const SQLCodeBlock = dynamic(() => import('@/components/code/SQLCodeBlock'), { ssr: false, }); -const { Paragraph, Title, Text } = Typography; +const { Paragraph, Text } = Typography; export default function ManageQuestionSQLPairs() { const questionSqlPairModal = useModalAction(); @@ -100,7 +101,7 @@ export default function ManageQuestionSQLPairs() { width: '60%', render: (sql) => (
- +
), }, @@ -125,12 +126,14 @@ export default function ManageQuestionSQLPairs() { return ( -
-
- + <PageLayout + title={ + <> <FunctionOutlined className="mr-2 gray-8" /> Manage question-SQL pairs - + + } + titleExtra={ -
- - On this page, you can manage your saved question-SQL pairs. These - pairs help Wren AI learn how your organization writes SQL, allowing it - to generate queries that better align with your expectations.{' '} - - Learn more. - - + } + description={ + <> + On this page, you can manage your saved question-SQL pairs. These + pairs help Wren AI learn how your organization writes SQL, allowing + it to generate queries that better align with your expectations.{' '} + + Learn more. + + + } + >
- + ); } diff --git a/wren-ui/src/utils/enum/index.ts b/wren-ui/src/utils/enum/index.ts index 7d65d7b4fe..7d56d20e9f 100644 --- a/wren-ui/src/utils/enum/index.ts +++ b/wren-ui/src/utils/enum/index.ts @@ -7,5 +7,5 @@ export * from './path'; export * from './diagram'; export * from './home'; export * from './settings'; -export * from './knowledge'; export * from './dropdown'; +export * from './menu'; diff --git a/wren-ui/src/utils/enum/knowledge.ts b/wren-ui/src/utils/enum/knowledge.ts deleted file mode 100644 index 6e60d8da70..0000000000 --- a/wren-ui/src/utils/enum/knowledge.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const KNOWLEDGE = { - QUESTION_SQL_PAIRS: 'question-sql-pairs', - INSTRUCTIONS: 'instructions', -}; diff --git a/wren-ui/src/utils/enum/menu.ts b/wren-ui/src/utils/enum/menu.ts new file mode 100644 index 0000000000..2d5e55c354 --- /dev/null +++ b/wren-ui/src/utils/enum/menu.ts @@ -0,0 +1,6 @@ +export enum MENU_KEY { + QUESTION_SQL_PAIRS = 'question-sql-pairs', + INSTRUCTIONS = 'instructions', + API_HISTORY = 'api-history', + API_REFERENCE = 'api-reference', +} diff --git a/wren-ui/src/utils/enum/path.ts b/wren-ui/src/utils/enum/path.ts index 241bfb2202..10f5fa8091 100644 --- a/wren-ui/src/utils/enum/path.ts +++ b/wren-ui/src/utils/enum/path.ts @@ -10,4 +10,6 @@ export enum Path { Knowledge = '/knowledge', KnowledgeQuestionSQLPairs = '/knowledge/question-sql-pairs', KnowledgeInstructions = '/knowledge/instructions', + APIManagement = '/api-management', + APIManagementHistory = '/api-management/history', } diff --git a/wren-ui/src/utils/icons.ts b/wren-ui/src/utils/icons.ts index 3a7fbf887e..a79eff7a59 100644 --- a/wren-ui/src/utils/icons.ts +++ b/wren-ui/src/utils/icons.ts @@ -21,6 +21,7 @@ import { Pageview, Explore, Translate, + OpenInNew, } from '@styled-icons/material-outlined'; import FieldBinaryOutlined from '@ant-design/icons/FieldBinaryOutlined'; import MonitorOutlined from '@ant-design/icons/MonitorOutlined'; @@ -114,3 +115,7 @@ export const GithubIcon = styled(Github)` export const TranslateIcon = styled(Translate)` height: 1em; `; + +export const OpenInNewIcon = styled(OpenInNew)` + height: 1em; +`; diff --git a/wren-ui/src/utils/table.tsx b/wren-ui/src/utils/table.tsx new file mode 100644 index 0000000000..ad69d3af75 --- /dev/null +++ b/wren-ui/src/utils/table.tsx @@ -0,0 +1,130 @@ +import { useEffect } from 'react'; +import moment from 'moment'; +import { Input, Button, Space, DatePicker, Divider } from 'antd'; +import SearchOutlined from '@ant-design/icons/SearchOutlined'; +import CalendarOutlined from '@ant-design/icons/CalendarOutlined'; + +export const getColumnSearchProps = (props: { + dataIndex: string; + placeholder?: string; + onFilter?: (value: string, record: any) => boolean; + filteredValue?: any[]; +}) => ({ + filterDropdown: (filters: any) => { + return ; + }, + filterIcon: (filtered: boolean) => ( + + ), + filteredValue: props.filteredValue, +}); + +export const getColumnDateFilterProps = (props: { + dataIndex: string; + onFilter?: (value: any, record: any) => boolean; + filteredValue?: [string, string] | null; +}) => ({ + filterDropdown: (filters) => { + return ; + }, + filterIcon: (filtered: boolean) => ( + + ), + filteredValue: props.filteredValue, +}); + +const SearchFilter = ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + visible, + dataIndex, + placeholder, + filteredValue, +}) => { + useEffect(() => { + if (!visible && selectedKeys.length === 0) confirm(); + }, [visible]); + return ( + <> + + + setSelectedKeys(e.target.value ? [e.target.value] : []) + } + onPressEnter={() => confirm()} + style={{ width: 188 }} + /> + + + + + + + + ); +}; + +const DateFilter = ({ + filteredValue, + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + visible, +}) => { + useEffect(() => { + if (!visible && selectedKeys.length === 0) confirm(); + }, [visible]); + return ( + <> + + { + const values = dates + ? [dates[0]?.format('YYYY-MM-DD'), dates[1]?.format('YYYY-MM-DD')] + : []; + setSelectedKeys(values); + }} + style={{ width: 250 }} + /> + + + + + + + + ); +}; diff --git a/wren-ui/src/utils/time.ts b/wren-ui/src/utils/time.ts index 6da6ba2fc6..2fa8a214ee 100644 --- a/wren-ui/src/utils/time.ts +++ b/wren-ui/src/utils/time.ts @@ -9,11 +9,11 @@ export const nextTick = (ms = 1) => new Promise((resolve) => setTimeout(resolve, ms)); export const getRelativeTime = (time: string) => { - return dayJs(time).utc().fromNow(); + return dayJs(time).fromNow(); }; export const getAbsoluteTime = (time: string) => { - return dayJs(time).utc().format('YYYY-MM-DD HH:mm:ss'); + return dayJs(time).format('YYYY-MM-DD HH:mm:ss'); }; export const getCompactTime = (time: string) => { @@ -21,9 +21,9 @@ export const getCompactTime = (time: string) => { }; export const getFullNameDate = (time: string) => { - return dayJs(time).utc().format('MMMM DD, YYYY'); + return dayJs(time).format('MMMM DD, YYYY'); }; export const getShortDate = (time: string) => { - return dayJs(time).utc().format('MMM DD'); + return dayJs(time).format('MMM DD'); };