mirror of
https://github.com/Canner/WrenAI.git
synced 2026-09-24 23:29:49 +08:00
chore(wren-ui): remove unused code (#555)
* chore(wren-ui): remove dead CSS style * chore(wren-ui): remove unused component * chore(wren-ui): remove connection info GraphQL API * chore(wren-ui): update bootstrap and launcher
This commit is contained in:
@@ -31,11 +31,6 @@ WREN_BOOTSTRAP_VERSION=0.1.4
|
||||
AI_SERVICE_ENABLE_TIMER=
|
||||
AI_SERVICE_LOGGING_LEVEL=INFO
|
||||
|
||||
# SQL Protocol
|
||||
PG_USERNAME=wren-user-placeholder
|
||||
# PG_PASSWORD will be replaced with random string by launcher
|
||||
PG_PASSWORD=wren-pg-password-placeholder
|
||||
|
||||
# user id (uuid v4)
|
||||
USER_UUID=
|
||||
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
#!/bin/sh
|
||||
# declare a variable from the environment variable: DATA_PATH
|
||||
data_path=${DATA_PATH:-"./"}
|
||||
# get default password from environment variable, if not exists, then throw error
|
||||
if [ -z "$PG_PASSWORD" ]; then
|
||||
echo "PG_PASSWORD is not set"
|
||||
exit 1
|
||||
fi
|
||||
pg_password=$PG_PASSWORD
|
||||
# get default username from environment variable, if not exists, then throw error
|
||||
if [ -z "$PG_USERNAME" ]; then
|
||||
echo "PG_USERNAME is not set"
|
||||
exit 1
|
||||
fi
|
||||
pg_username=$PG_USERNAME
|
||||
|
||||
# touch a empty config.properties if not exists
|
||||
# put a content into config.properties if not exists
|
||||
@@ -40,11 +28,3 @@ if [ ! -f ${data_path}/mdl/sample.json ]; then
|
||||
echo "init mdl/sample.json"
|
||||
echo "{\"catalog\": \"test_catalog\", \"schema\": \"test_schema\", \"models\": []}" > ${data_path}/mdl/sample.json
|
||||
fi
|
||||
|
||||
# put a accounts file in data_path if not exists
|
||||
# accounts is a file contains the authentication information
|
||||
# the format is like: `username password` per line
|
||||
if [ ! -f ${data_path}/accounts ]; then
|
||||
echo "init accounts file"
|
||||
echo "${pg_username} ${pg_password}" > ${data_path}/accounts
|
||||
fi
|
||||
|
||||
@@ -14,8 +14,6 @@ services:
|
||||
platform: ${PLATFORM}
|
||||
environment:
|
||||
DATA_PATH: /app/data
|
||||
PG_USERNAME: ${PG_USERNAME}
|
||||
PG_PASSWORD: ${PG_PASSWORD}
|
||||
volumes:
|
||||
- data:/app/data
|
||||
command: /bin/sh /app/init.sh
|
||||
|
||||
@@ -14,8 +14,6 @@ services:
|
||||
platform: ${PLATFORM}
|
||||
environment:
|
||||
DATA_PATH: /app/data
|
||||
PG_USERNAME: ${PG_USERNAME}
|
||||
PG_PASSWORD: ${PG_PASSWORD}
|
||||
volumes:
|
||||
- data:/app/data
|
||||
command: /bin/sh /app/init.sh
|
||||
@@ -97,8 +95,6 @@ services:
|
||||
EMBEDDING_MODEL: ${EMBEDDING_MODEL}
|
||||
EMBEDDING_MODEL_DIMENSION: ${EMBEDDING_MODEL_DIMENSION}
|
||||
GENERATION_MODEL: ${GENERATION_MODEL}
|
||||
PG_USERNAME: ${PG_USERNAME}
|
||||
PG_PASSWORD: ${PG_PASSWORD}
|
||||
# telemetry
|
||||
WREN_ENGINE_PORT: ${WREN_ENGINE_PORT}
|
||||
WREN_AI_SERVICE_VERSION: ${WREN_AI_SERVICE_VERSION}
|
||||
|
||||
@@ -17,10 +17,6 @@ WREN_UI_VERSION=latest
|
||||
IBIS_SERVER_VERSION=latest
|
||||
WREN_BOOTSTRAP_VERSION=latest
|
||||
|
||||
# SQL Protocol
|
||||
PG_USERNAME=wren-user-placeholder
|
||||
# PG_PASSWORD will be replaced with random string by launcher
|
||||
PG_PASSWORD=wren-pg-password-placeholder
|
||||
LAUNCH_CLI_PATH=./launch-cli.sh
|
||||
|
||||
# user id (uuid v4)
|
||||
|
||||
@@ -187,7 +187,6 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pterm/pterm v0.12.79
|
||||
github.com/sethvargo/go-password v0.2.0
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
|
||||
@@ -484,8 +484,6 @@ github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
|
||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
|
||||
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
|
||||
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
|
||||
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
|
||||
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pterm/pterm"
|
||||
|
||||
"github.com/sethvargo/go-password/password"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,12 +26,9 @@ const (
|
||||
DOCKER_COMPOSE_YAML_URL string = "https://raw.githubusercontent.com/Canner/WrenAI/" + WREN_PRODUCT_VERSION + "/docker/docker-compose.yaml"
|
||||
DOCKER_COMPOSE_LLM_YAML_URL string = "https://raw.githubusercontent.com/Canner/WrenAI/" + WREN_PRODUCT_VERSION + "/docker/docker-compose.llm.yaml"
|
||||
DOCKER_COMPOSE_ENV_URL string = "https://raw.githubusercontent.com/Canner/WrenAI/" + WREN_PRODUCT_VERSION + "/docker/.env.example"
|
||||
|
||||
// pg user
|
||||
PG_USERNAME string = "wren-user"
|
||||
)
|
||||
|
||||
func replaceEnvFileContent(content string, projectDir string, openaiApiKey string, openAIGenerationModel string, hostPort int, aiPort int, pg_password string, userUUID string, telemetryEnabled bool) string {
|
||||
func replaceEnvFileContent(content string, projectDir string, openaiApiKey string, openAIGenerationModel string, hostPort int, aiPort int, userUUID string, telemetryEnabled bool) string {
|
||||
// rplace PROJECT_DIR
|
||||
reg := regexp.MustCompile(`PROJECT_DIR=(.*)`)
|
||||
str := reg.ReplaceAllString(content, "PROJECT_DIR="+projectDir)
|
||||
@@ -65,14 +60,6 @@ func replaceEnvFileContent(content string, projectDir string, openaiApiKey strin
|
||||
reg = regexp.MustCompile(`AI_SERVICE_FORWARD_PORT=(.*)`)
|
||||
str = reg.ReplaceAllString(str, "AI_SERVICE_FORWARD_PORT="+fmt.Sprintf("%d", aiPort))
|
||||
|
||||
// replace PG_PASSWORD
|
||||
reg = regexp.MustCompile(`PG_PASSWORD=(.*)`)
|
||||
str = reg.ReplaceAllString(str, "PG_PASSWORD="+pg_password)
|
||||
|
||||
// replace PG_USERNAME
|
||||
reg = regexp.MustCompile(`PG_USERNAME=(.*)`)
|
||||
str = reg.ReplaceAllString(str, "PG_USERNAME="+PG_USERNAME)
|
||||
|
||||
// replace TELEMETRY_ENABLED
|
||||
reg = regexp.MustCompile(`TELEMETRY_ENABLED=(.*)`)
|
||||
str = reg.ReplaceAllString(str, "TELEMETRY_ENABLED="+fmt.Sprintf("%t", telemetryEnabled))
|
||||
@@ -101,35 +88,6 @@ func downloadFile(filepath string, url string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func getPGPassword(w WrenRC) (string, error) {
|
||||
pgPwdKey := "pg_password"
|
||||
// get the password from rc file if exists
|
||||
// if not exists, generate a new password
|
||||
pgPwd, err := w.Read(pgPwdKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pgPwd == "" {
|
||||
genPwd, err := password.Generate(10, 2, 0, false, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// set the password to rc file
|
||||
err = w.Set(pgPwdKey, genPwd, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// return the generated password
|
||||
return genPwd, nil
|
||||
}
|
||||
|
||||
// return the password from rc file
|
||||
return pgPwd, nil
|
||||
}
|
||||
|
||||
func CheckDockerDaemonRunning() (bool, error) {
|
||||
ctx := context.Background()
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
@@ -182,11 +140,6 @@ func PrepareDockerFiles(openaiApiKey string, openaiGenerationModel string, hostP
|
||||
return err
|
||||
}
|
||||
|
||||
pg_pwd, err := getPGPassword(WrenRC{projectDir})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userUUID, err := prepareUserUUID(projectDir)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -214,7 +167,6 @@ func PrepareDockerFiles(openaiApiKey string, openaiGenerationModel string, hostP
|
||||
openaiGenerationModel,
|
||||
hostPort,
|
||||
aiPort,
|
||||
pg_pwd,
|
||||
userUUID,
|
||||
telemetryEnabled,
|
||||
)
|
||||
|
||||
@@ -62,15 +62,6 @@ export type CompactTable = {
|
||||
properties?: Maybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
export type ConnectionInfo = {
|
||||
__typename?: 'ConnectionInfo';
|
||||
database: Scalars['String'];
|
||||
password?: Maybe<Scalars['String']>;
|
||||
port: Scalars['Int'];
|
||||
schema: Scalars['String'];
|
||||
username?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type CreateCalculatedFieldInput = {
|
||||
expression: ExpressionName;
|
||||
lineage: Array<Scalars['Int']>;
|
||||
@@ -642,7 +633,6 @@ export type Query = {
|
||||
__typename?: 'Query';
|
||||
askingTask: AskingTask;
|
||||
autoGenerateRelation: Array<RecommendRelations>;
|
||||
connectionInfo: ConnectionInfo;
|
||||
diagram: Diagram;
|
||||
getMDL: GetMdlResult;
|
||||
listDataSourceTables: Array<CompactTable>;
|
||||
|
||||
@@ -13,11 +13,6 @@ export type DeployStatusQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type DeployStatusQuery = { __typename?: 'Query', modelSync: { __typename?: 'ModelSyncResponse', status: Types.SyncStatus } };
|
||||
|
||||
export type ConnectionInfoQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ConnectionInfoQuery = { __typename?: 'Query', connectionInfo: { __typename?: 'ConnectionInfo', database: string, schema: string, port: number, username?: string | null, password?: string | null } };
|
||||
|
||||
|
||||
export const DeployDocument = gql`
|
||||
mutation Deploy {
|
||||
@@ -82,42 +77,4 @@ export function useDeployStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
|
||||
}
|
||||
export type DeployStatusQueryHookResult = ReturnType<typeof useDeployStatusQuery>;
|
||||
export type DeployStatusLazyQueryHookResult = ReturnType<typeof useDeployStatusLazyQuery>;
|
||||
export type DeployStatusQueryResult = Apollo.QueryResult<DeployStatusQuery, DeployStatusQueryVariables>;
|
||||
export const ConnectionInfoDocument = gql`
|
||||
query ConnectionInfo {
|
||||
connectionInfo {
|
||||
database
|
||||
schema
|
||||
port
|
||||
username
|
||||
password
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useConnectionInfoQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useConnectionInfoQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useConnectionInfoQuery` 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 } = useConnectionInfoQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useConnectionInfoQuery(baseOptions?: Apollo.QueryHookOptions<ConnectionInfoQuery, ConnectionInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ConnectionInfoQuery, ConnectionInfoQueryVariables>(ConnectionInfoDocument, options);
|
||||
}
|
||||
export function useConnectionInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ConnectionInfoQuery, ConnectionInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ConnectionInfoQuery, ConnectionInfoQueryVariables>(ConnectionInfoDocument, options);
|
||||
}
|
||||
export type ConnectionInfoQueryHookResult = ReturnType<typeof useConnectionInfoQuery>;
|
||||
export type ConnectionInfoLazyQueryHookResult = ReturnType<typeof useConnectionInfoLazyQuery>;
|
||||
export type ConnectionInfoQueryResult = Apollo.QueryResult<ConnectionInfoQuery, ConnectionInfoQueryVariables>;
|
||||
export type DeployStatusQueryResult = Apollo.QueryResult<DeployStatusQuery, DeployStatusQueryVariables>;
|
||||
@@ -13,15 +13,3 @@ export const GET_DEPLOY_STATUS = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CONNECTION_INFO = gql`
|
||||
query ConnectionInfo {
|
||||
connectionInfo {
|
||||
database
|
||||
schema
|
||||
port
|
||||
username
|
||||
password
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -28,10 +28,6 @@ export interface IConfig {
|
||||
encryptionPassword: string;
|
||||
encryptionSalt: string;
|
||||
|
||||
// username and password
|
||||
pgUsername?: string;
|
||||
pgPassword?: string;
|
||||
|
||||
// telemetry
|
||||
telemetryEnabled?: boolean;
|
||||
posthogApiKey?: string;
|
||||
@@ -111,10 +107,6 @@ const config = {
|
||||
encryptionPassword: process.env.ENCRYPTION_PASSWORD,
|
||||
encryptionSalt: process.env.ENCRYPTION_SALT,
|
||||
|
||||
// username and password
|
||||
pgUsername: process.env.PG_USERNAME,
|
||||
pgPassword: process.env.PG_PASSWORD,
|
||||
|
||||
// telemetry
|
||||
telemetryEnabled:
|
||||
process.env.TELEMETRY_ENABLED &&
|
||||
|
||||
@@ -3,13 +3,11 @@ import { ProjectResolver } from './resolvers/projectResolver';
|
||||
import { ModelResolver } from './resolvers/modelResolver';
|
||||
import { AskingResolver } from './resolvers/askingResolver';
|
||||
import { DiagramResolver } from './resolvers/diagramResolver';
|
||||
import { ConnectionInfoResolver } from './resolvers/connectionInfoResolver';
|
||||
|
||||
const projectResolver = new ProjectResolver();
|
||||
const modelResolver = new ModelResolver();
|
||||
const askingResolver = new AskingResolver();
|
||||
const diagramResolver = new DiagramResolver();
|
||||
const connectionInfoResolver = new ConnectionInfoResolver();
|
||||
|
||||
const resolvers = {
|
||||
JSON: GraphQLJSON,
|
||||
@@ -37,9 +35,6 @@ const resolvers = {
|
||||
listViews: modelResolver.listViews,
|
||||
view: modelResolver.getView,
|
||||
|
||||
// Connection Info
|
||||
connectionInfo: connectionInfoResolver.connectionInfo,
|
||||
|
||||
// Settings
|
||||
settings: projectResolver.getSettings,
|
||||
getMDL: modelResolver.getMDL,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { IContext } from '../types';
|
||||
import { getConfig } from '../config';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
export interface ConnectionInfo {
|
||||
port: number;
|
||||
database: string;
|
||||
schema: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class ConnectionInfoResolver {
|
||||
constructor() {
|
||||
this.connectionInfo = this.connectionInfo.bind(this);
|
||||
}
|
||||
|
||||
public async connectionInfo(
|
||||
_root: any,
|
||||
_args: any,
|
||||
ctx: IContext,
|
||||
): Promise<ConnectionInfo> {
|
||||
const project = await ctx.projectService.getCurrentProject();
|
||||
|
||||
return {
|
||||
port: 7432,
|
||||
database: project.catalog,
|
||||
schema: project.schema,
|
||||
username: config.pgUsername,
|
||||
password: config.pgPassword,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -588,15 +588,6 @@ export const typeDefs = gql`
|
||||
questions: [SuggestedQuestion]!
|
||||
}
|
||||
|
||||
# SQL protocol connection information
|
||||
type ConnectionInfo {
|
||||
port: Int!
|
||||
database: String!
|
||||
schema: String!
|
||||
username: String
|
||||
password: String
|
||||
}
|
||||
|
||||
# Settings
|
||||
input UpdateDataSourceInput {
|
||||
properties: JSON!
|
||||
@@ -682,9 +673,6 @@ export const typeDefs = gql`
|
||||
threadResponse(responseId: Int!): ThreadResponse!
|
||||
nativeSql(responseId: Int!): String!
|
||||
|
||||
# Connection Info
|
||||
connectionInfo: ConnectionInfo!
|
||||
|
||||
# Settings
|
||||
settings: Settings!
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Button, Input, Popover, Space, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { ShareIcon } from '@/utils/icons';
|
||||
import { useConnectionInfoQuery } from '@/apollo/client/graphql/deploy.generated';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const Content = styled.div`
|
||||
width: 423px;
|
||||
padding: 4px 0;
|
||||
.adm-share-title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.adm-share-subtitle {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInput = styled(Input)`
|
||||
display: block;
|
||||
color: var(--gray-10);
|
||||
.ant-input {
|
||||
background-color: var(--gray-4);
|
||||
}
|
||||
|
||||
.ant-typography-copy {
|
||||
color: var(--gray-8);
|
||||
}
|
||||
|
||||
.ant-typography-copy-success:focus {
|
||||
color: var(--green-6);
|
||||
}
|
||||
`;
|
||||
|
||||
export default function SharePopover() {
|
||||
const { data } = useConnectionInfoQuery();
|
||||
const connections = data?.connectionInfo;
|
||||
|
||||
const sources = [
|
||||
{ title: 'Database', type: 'text', value: connections?.database },
|
||||
{ title: 'Port', type: 'text', value: String(connections?.port || '') },
|
||||
{ title: 'Username', type: 'text', value: connections?.username },
|
||||
{ title: 'Password', type: 'password', value: connections?.password },
|
||||
];
|
||||
|
||||
const content = (
|
||||
<Content>
|
||||
<Title className="adm-share-title">
|
||||
<Space>
|
||||
<ShareIcon />
|
||||
Share
|
||||
</Space>
|
||||
</Title>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
You can connect applications via our protocol.
|
||||
</div>
|
||||
|
||||
<Space style={{ width: '100%' }} direction="vertical" size={[0, 16]}>
|
||||
{sources.map(({ title, type, value }) => (
|
||||
<div key={title}>
|
||||
<div className="adm-share-subtitle">{title}</div>
|
||||
<StyledInput
|
||||
type={type}
|
||||
readOnly
|
||||
value={value}
|
||||
addonAfter={
|
||||
<Text
|
||||
copyable={{ text: value, tooltips: ['Copy', 'Copied!'] }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</Content>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={content} trigger="click" placement="bottomRight">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className="adm-modeling-header-btn"
|
||||
data-ph-capture="true"
|
||||
data-ph-capture-attribute-name="cta_share_btn"
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,3 @@
|
||||
.adm-btn {
|
||||
&-gray {
|
||||
.button-color(@gray-9; @gray-6; @gray-6);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
.button-color(@gray-9; @gray-5; @gray-5);
|
||||
}
|
||||
|
||||
&:active {
|
||||
.button-color(@gray-9; @gray-7; @gray-7);
|
||||
}
|
||||
|
||||
.button-disabled();
|
||||
}
|
||||
}
|
||||
|
||||
.adm-btn-no-style {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
|
||||
Reference in New Issue
Block a user