Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2272e4f247 | ||
|
|
f33973bc24 | ||
|
|
777a031fd2 | ||
|
|
06d8719e07 | ||
|
|
91b0937315 | ||
|
|
c30d4ea8f5 | ||
|
|
c5c77bed7d | ||
|
|
00e6f55c8d | ||
|
|
bbf875d21d | ||
|
|
99da104d94 |
@@ -70,6 +70,27 @@ export type AdminAiModels = {
|
||||
models: Array<AdminAiModelConfig>;
|
||||
};
|
||||
|
||||
export type AdminChatMessage = {
|
||||
__typename?: 'AdminChatMessage';
|
||||
createdAt: Scalars['DateTime'];
|
||||
id: Scalars['UUID'];
|
||||
parts: Array<AdminChatMessagePart>;
|
||||
role: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AdminChatMessagePart = {
|
||||
__typename?: 'AdminChatMessagePart';
|
||||
textContent?: Maybe<Scalars['String']>;
|
||||
toolName?: Maybe<Scalars['String']>;
|
||||
type: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AdminChatThreadMessages = {
|
||||
__typename?: 'AdminChatThreadMessages';
|
||||
messages: Array<AdminChatMessage>;
|
||||
thread: AdminWorkspaceChatThread;
|
||||
};
|
||||
|
||||
export type AdminPanelHealthServiceData = {
|
||||
__typename?: 'AdminPanelHealthServiceData';
|
||||
description: Scalars['String'];
|
||||
@@ -86,6 +107,25 @@ export enum AdminPanelHealthServiceStatus {
|
||||
OUTAGE = 'OUTAGE'
|
||||
}
|
||||
|
||||
export type AdminPanelRecentUser = {
|
||||
__typename?: 'AdminPanelRecentUser';
|
||||
createdAt: Scalars['DateTime'];
|
||||
email: Scalars['String'];
|
||||
firstName?: Maybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
lastName?: Maybe<Scalars['String']>;
|
||||
workspaceId?: Maybe<Scalars['UUID']>;
|
||||
workspaceName?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type AdminPanelTopWorkspace = {
|
||||
__typename?: 'AdminPanelTopWorkspace';
|
||||
id: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
subdomain: Scalars['String'];
|
||||
totalUsers: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type AdminPanelWorkerQueueHealth = {
|
||||
__typename?: 'AdminPanelWorkerQueueHealth';
|
||||
id: Scalars['String'];
|
||||
@@ -93,6 +133,17 @@ export type AdminPanelWorkerQueueHealth = {
|
||||
status: AdminPanelHealthServiceStatus;
|
||||
};
|
||||
|
||||
export type AdminWorkspaceChatThread = {
|
||||
__typename?: 'AdminWorkspaceChatThread';
|
||||
conversationSize: Scalars['Int'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
id: Scalars['UUID'];
|
||||
title?: Maybe<Scalars['String']>;
|
||||
totalInputTokens: Scalars['Int'];
|
||||
totalOutputTokens: Scalars['Int'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type Agent = {
|
||||
__typename?: 'Agent';
|
||||
applicationId?: Maybe<Scalars['UUID']>;
|
||||
@@ -4314,6 +4365,8 @@ export type PublicWorkspaceData = {
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
adminPanelRecentUsers: Array<AdminPanelRecentUser>;
|
||||
adminPanelTopWorkspaces: Array<AdminPanelTopWorkspace>;
|
||||
agentTurns: Array<AgentTurn>;
|
||||
apiKey?: Maybe<ApiKey>;
|
||||
apiKeys: Array<ApiKey>;
|
||||
@@ -4362,6 +4415,8 @@ export type Query = {
|
||||
getAddressDetails: PlaceDetailsResult;
|
||||
getAdminAiModels: AdminAiModels;
|
||||
getAdminAiUsageByWorkspace: Array<UsageBreakdownItem>;
|
||||
getAdminChatThreadMessages: AdminChatThreadMessages;
|
||||
getAdminWorkspaceChatThreads: Array<AdminWorkspaceChatThread>;
|
||||
getAiProviders: Scalars['JSON'];
|
||||
getApprovedAccessDomains: Array<ApprovedAccessDomain>;
|
||||
getAutoCompleteAddress: Array<AutocompleteResult>;
|
||||
@@ -4427,6 +4482,17 @@ export type Query = {
|
||||
versionInfo: VersionInfo;
|
||||
webhook?: Maybe<Webhook>;
|
||||
webhooks: Array<Webhook>;
|
||||
workspaceLookupAdminPanel: UserLookup;
|
||||
};
|
||||
|
||||
|
||||
export type QueryAdminPanelRecentUsersArgs = {
|
||||
searchTerm?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryAdminPanelTopWorkspacesArgs = {
|
||||
searchTerm?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -4592,6 +4658,17 @@ export type QueryGetAdminAiUsageByWorkspaceArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetAdminChatThreadMessagesArgs = {
|
||||
threadId: Scalars['String'];
|
||||
workspaceId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetAdminWorkspaceChatThreadsArgs = {
|
||||
workspaceId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetAutoCompleteAddressArgs = {
|
||||
address: Scalars['String'];
|
||||
country?: InputMaybe<Scalars['String']>;
|
||||
@@ -4827,6 +4904,11 @@ export type QueryWebhookArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceLookupAdminPanelArgs = {
|
||||
workspaceId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type QueueJob = {
|
||||
__typename?: 'QueueJob';
|
||||
attemptsMade: Scalars['Float'];
|
||||
@@ -5850,6 +5932,7 @@ export type User = {
|
||||
|
||||
export type UserInfo = {
|
||||
__typename?: 'UserInfo';
|
||||
createdAt: Scalars['DateTime'];
|
||||
email: Scalars['String'];
|
||||
firstName?: Maybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
@@ -6258,7 +6341,9 @@ export enum WorkspaceActivationStatus {
|
||||
|
||||
export type WorkspaceInfo = {
|
||||
__typename?: 'WorkspaceInfo';
|
||||
activationStatus: WorkspaceActivationStatus;
|
||||
allowImpersonation: Scalars['Boolean'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
featureFlags: Array<FeatureFlag>;
|
||||
id: Scalars['UUID'];
|
||||
logo?: Maybe<Scalars['String']>;
|
||||
@@ -7381,13 +7466,49 @@ export type UserLookupAdminPanelMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
|
||||
export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, allowImpersonation: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
|
||||
|
||||
export type AdminPanelRecentUsersQueryVariables = Exact<{
|
||||
searchTerm?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type AdminPanelRecentUsersQuery = { __typename?: 'Query', adminPanelRecentUsers: Array<{ __typename?: 'AdminPanelRecentUser', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string, workspaceName?: string | null, workspaceId?: string | null }> };
|
||||
|
||||
export type AdminPanelTopWorkspacesQueryVariables = Exact<{
|
||||
searchTerm?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type AdminPanelTopWorkspacesQuery = { __typename?: 'Query', adminPanelTopWorkspaces: Array<{ __typename?: 'AdminPanelTopWorkspace', id: string, name: string, totalUsers: number, subdomain: string }> };
|
||||
|
||||
export type GetAdminChatThreadMessagesQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String'];
|
||||
threadId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAdminChatThreadMessagesQuery = { __typename?: 'Query', getAdminChatThreadMessages: { __typename?: 'AdminChatThreadMessages', thread: { __typename?: 'AdminWorkspaceChatThread', id: string, title?: string | null, totalInputTokens: number, totalOutputTokens: number, conversationSize: number, createdAt: string, updatedAt: string }, messages: Array<{ __typename?: 'AdminChatMessage', id: string, role: string, createdAt: string, parts: Array<{ __typename?: 'AdminChatMessagePart', type: string, textContent?: string | null, toolName?: string | null }> }> } };
|
||||
|
||||
export type GetAdminWorkspaceChatThreadsQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAdminWorkspaceChatThreadsQuery = { __typename?: 'Query', getAdminWorkspaceChatThreads: Array<{ __typename?: 'AdminWorkspaceChatThread', id: string, title?: string | null, totalInputTokens: number, totalOutputTokens: number, conversationSize: number, createdAt: string, updatedAt: string }> };
|
||||
|
||||
export type GetVersionInfoQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetVersionInfoQuery = { __typename?: 'Query', versionInfo: { __typename?: 'VersionInfo', currentVersion?: string | null, latestVersion: string } };
|
||||
|
||||
export type WorkspaceLookupAdminPanelQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type WorkspaceLookupAdminPanelQuery = { __typename?: 'Query', workspaceLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, allowImpersonation: boolean, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
|
||||
|
||||
export type DeleteJobsMutationVariables = Exact<{
|
||||
queueName: Scalars['String'];
|
||||
jobIds: Array<Scalars['String']> | Scalars['String'];
|
||||
@@ -8562,8 +8683,13 @@ export const UpdateDatabaseConfigVariableDocument = {"kind":"Document","definiti
|
||||
export const GetConfigVariablesGroupedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConfigVariablesGrouped"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getConfigVariablesGrouped"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groups"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isHiddenOnLoad"}},{"kind":"Field","name":{"kind":"Name","value":"variables"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"isSensitive"}},{"kind":"Field","name":{"kind":"Name","value":"isEnvOnly"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"source"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>;
|
||||
export const GetDatabaseConfigVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDatabaseConfigVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getDatabaseConfigVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"isSensitive"}},{"kind":"Field","name":{"kind":"Name","value":"isEnvOnly"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"source"}}]}}]}}]} as unknown as DocumentNode<GetDatabaseConfigVariableQuery, GetDatabaseConfigVariableQueryVariables>;
|
||||
export const UpdateWorkspaceFeatureFlagDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceFeatureFlag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"featureFlag"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"value"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateWorkspaceFeatureFlag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"featureFlag"},"value":{"kind":"Variable","name":{"kind":"Name","value":"featureFlag"}}},{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"value"}}}]}]}}]} as unknown as DocumentNode<UpdateWorkspaceFeatureFlagMutation, UpdateWorkspaceFeatureFlagMutationVariables>;
|
||||
export const UserLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UserLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UserLookupAdminPanelMutation, UserLookupAdminPanelMutationVariables>;
|
||||
export const UserLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UserLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UserLookupAdminPanelMutation, UserLookupAdminPanelMutationVariables>;
|
||||
export const AdminPanelRecentUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminPanelRecentUsers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminPanelRecentUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"searchTerm"},"value":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}}]}}]}}]} as unknown as DocumentNode<AdminPanelRecentUsersQuery, AdminPanelRecentUsersQueryVariables>;
|
||||
export const AdminPanelTopWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminPanelTopWorkspaces"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminPanelTopWorkspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"searchTerm"},"value":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"subdomain"}}]}}]}}]} as unknown as DocumentNode<AdminPanelTopWorkspacesQuery, AdminPanelTopWorkspacesQueryVariables>;
|
||||
export const GetAdminChatThreadMessagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminChatThreadMessages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminChatThreadMessages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"thread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"messages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"parts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"textContent"}},{"kind":"Field","name":{"kind":"Name","value":"toolName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<GetAdminChatThreadMessagesQuery, GetAdminChatThreadMessagesQueryVariables>;
|
||||
export const GetAdminWorkspaceChatThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminWorkspaceChatThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminWorkspaceChatThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<GetAdminWorkspaceChatThreadsQuery, GetAdminWorkspaceChatThreadsQueryVariables>;
|
||||
export const GetVersionInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetVersionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}}]}}]}}]} as unknown as DocumentNode<GetVersionInfoQuery, GetVersionInfoQueryVariables>;
|
||||
export const WorkspaceLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}}]} as unknown as DocumentNode<WorkspaceLookupAdminPanelQuery, WorkspaceLookupAdminPanelQueryVariables>;
|
||||
export const DeleteJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"jobIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletedCount"}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobId"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode<DeleteJobsMutation, DeleteJobsMutationVariables>;
|
||||
export const RetryJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RetryJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"retryJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"jobIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"retriedCount"}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobId"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode<RetryJobsMutation, RetryJobsMutationVariables>;
|
||||
export const GetIndicatorHealthStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetIndicatorHealthStatus"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"indicatorId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"HealthIndicatorId"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getIndicatorHealthStatus"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"indicatorId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"indicatorId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"details"}},{"kind":"Field","name":{"kind":"Name","value":"queues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"queueName"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<GetIndicatorHealthStatusQuery, GetIndicatorHealthStatusQueryVariables>;
|
||||
|
||||
@@ -425,6 +425,30 @@ const SettingsAdminNewAiModel = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminUserDetail = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdminUserDetail').then(
|
||||
(module) => ({
|
||||
default: module.SettingsAdminUserDetail,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminWorkspaceDetail = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdminWorkspaceDetail').then(
|
||||
(module) => ({
|
||||
default: module.SettingsAdminWorkspaceDetail,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminWorkspaceChatThread = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdminWorkspaceChatThread').then(
|
||||
(module) => ({
|
||||
default: module.SettingsAdminWorkspaceChatThread,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsUpdates = lazy(() =>
|
||||
import('~/pages/settings/updates/SettingsUpdates').then((module) => ({
|
||||
default: module.SettingsUpdates,
|
||||
@@ -770,6 +794,18 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
path={SettingsPath.AdminPanelAiProviderDetail}
|
||||
element={<SettingsAdminAiProviderDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AdminPanelUserDetail}
|
||||
element={<SettingsAdminUserDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AdminPanelWorkspaceDetail}
|
||||
element={<SettingsAdminWorkspaceDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AdminPanelWorkspaceChatThread}
|
||||
element={<SettingsAdminWorkspaceChatThread />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { useClearSseClient } from '@/sse-db-event/hooks/useClearSseClient';
|
||||
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
|
||||
import { type AuthTokenPair } from '~/generated-metadata/graphql';
|
||||
|
||||
const IMPERSONATION_SESSION_KEY = 'impersonation_original_session';
|
||||
|
||||
type StoredImpersonationSession = {
|
||||
tokenPair: AuthTokenPair;
|
||||
returnPath: string;
|
||||
};
|
||||
|
||||
export const useImpersonationSession = () => {
|
||||
const store = useStore();
|
||||
const client = useApolloClient();
|
||||
const navigate = useNavigate();
|
||||
const { getAuthTokensFromLoginToken, signOut } = useAuth();
|
||||
const { clearSseClient } = useClearSseClient();
|
||||
const { loadCurrentUser } = useLoadCurrentUser();
|
||||
|
||||
const startImpersonating = useCallback(
|
||||
async (loginToken: string, returnPath?: string) => {
|
||||
const currentTokenPair = store.get(tokenPairState.atom);
|
||||
|
||||
if (currentTokenPair) {
|
||||
const session: StoredImpersonationSession = {
|
||||
tokenPair: currentTokenPair,
|
||||
returnPath: returnPath ?? window.location.pathname,
|
||||
};
|
||||
sessionStorage.setItem(
|
||||
IMPERSONATION_SESSION_KEY,
|
||||
JSON.stringify(session),
|
||||
);
|
||||
}
|
||||
|
||||
clearSseClient();
|
||||
await client.clearStore();
|
||||
|
||||
store.set(isAppEffectRedirectEnabledState.atom, false);
|
||||
await getAuthTokensFromLoginToken(loginToken);
|
||||
store.set(isAppEffectRedirectEnabledState.atom, true);
|
||||
},
|
||||
[store, client, clearSseClient, getAuthTokensFromLoginToken],
|
||||
);
|
||||
|
||||
const stopImpersonating = useCallback(async () => {
|
||||
const raw = sessionStorage.getItem(IMPERSONATION_SESSION_KEY);
|
||||
|
||||
if (!raw) {
|
||||
// No stored session — likely a cross-workspace tab opened via redirect.
|
||||
// Try closing the tab (works when opened via window.open or target=_blank).
|
||||
window.close();
|
||||
// If window.close() was blocked by the browser, fall back to sign out.
|
||||
await signOut();
|
||||
return;
|
||||
}
|
||||
|
||||
let session: StoredImpersonationSession;
|
||||
|
||||
try {
|
||||
session = JSON.parse(raw);
|
||||
} catch {
|
||||
sessionStorage.removeItem(IMPERSONATION_SESSION_KEY);
|
||||
await signOut();
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.removeItem(IMPERSONATION_SESSION_KEY);
|
||||
|
||||
clearSseClient();
|
||||
await client.clearStore();
|
||||
|
||||
store.set(isAppEffectRedirectEnabledState.atom, false);
|
||||
store.set(tokenPairState.atom, session.tokenPair);
|
||||
|
||||
await loadCurrentUser();
|
||||
|
||||
store.set(isAppEffectRedirectEnabledState.atom, true);
|
||||
|
||||
navigate(session.returnPath);
|
||||
}, [store, client, clearSseClient, loadCurrentUser, signOut, navigate]);
|
||||
|
||||
const hasStoredSession = useCallback(() => {
|
||||
return sessionStorage.getItem(IMPERSONATION_SESSION_KEY) !== null;
|
||||
}, []);
|
||||
|
||||
return { startImpersonating, stopImpersonating, hasStoredSession };
|
||||
};
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useImpersonationSession } from '@/auth/hooks/useImpersonationSession';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { isImpersonatingState } from '@/auth/states/isImpersonatingState';
|
||||
import { InformationBanner } from '@/information-banner/components/InformationBanner';
|
||||
@@ -11,7 +11,7 @@ export const InformationBannerIsImpersonating = () => {
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const isImpersonating = useAtomStateValue(isImpersonatingState);
|
||||
|
||||
const { signOut } = useAuth();
|
||||
const { stopImpersonating } = useImpersonationSession();
|
||||
|
||||
if (!isDefined(currentWorkspaceMember) || !isImpersonating) {
|
||||
return null;
|
||||
@@ -25,7 +25,7 @@ export const InformationBannerIsImpersonating = () => {
|
||||
message={t`Logged in as ${impersonatedUser}`}
|
||||
buttonTitle={t`Stop impersonating`}
|
||||
buttonIcon={IconLogout}
|
||||
buttonOnClick={signOut}
|
||||
buttonOnClick={stopImpersonating}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+5
@@ -2,6 +2,8 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { H2Title, IconBolt, IconLock, IconRobot } from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
@@ -319,6 +321,9 @@ export const SettingsAdminAI = () => {
|
||||
<TableRow
|
||||
key={item.key}
|
||||
gridTemplateColumns={USAGE_TABLE_GRID_TEMPLATE_COLUMNS}
|
||||
to={getSettingsPath(SettingsPath.AdminPanelWorkspaceDetail, {
|
||||
workspaceId: item.key,
|
||||
})}
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
{item.label ?? item.key}
|
||||
|
||||
+139
-158
@@ -1,134 +1,73 @@
|
||||
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
|
||||
import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent';
|
||||
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
|
||||
import { ADMIN_PANEL_RECENT_USERS } from '@/settings/admin-panel/graphql/queries/adminPanelRecentUsers';
|
||||
import { ADMIN_PANEL_TOP_WORKSPACES } from '@/settings/admin-panel/graphql/queries/adminPanelTopWorkspaces';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useState } from 'react';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { UserLookupAdminPanelDocument } from '~/generated-metadata/graphql';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
|
||||
import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
H2Title,
|
||||
IconId,
|
||||
IconMail,
|
||||
IconSearch,
|
||||
IconUser,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
const StyledEmptyState = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
padding: ${themeCssVariables.spacing[4]} 0;
|
||||
`;
|
||||
|
||||
export const SettingsAdminGeneral = () => {
|
||||
const [userIdentifier, setUserIdentifier] = useState('');
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const [userSearchTerm, setUserSearchTerm] = useState('');
|
||||
const [debouncedUserSearchTerm] = useDebounce(userSearchTerm, 300);
|
||||
|
||||
const [activeTabId, setActiveTabId] = useAtomComponentState(
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID,
|
||||
);
|
||||
const [userLookupResult, setUserLookupResult] = useAtomState(
|
||||
userLookupResultState,
|
||||
);
|
||||
const [isUserLookupLoading, setIsUserLookupLoading] = useState(false);
|
||||
|
||||
const [userLookup] = useMutation(UserLookupAdminPanelDocument);
|
||||
const [workspaceSearchTerm, setWorkspaceSearchTerm] = useState('');
|
||||
const [debouncedWorkspaceSearchTerm] = useDebounce(workspaceSearchTerm, 300);
|
||||
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
|
||||
const canAccessFullAdminPanel = currentUser?.canAccessFullAdminPanel;
|
||||
|
||||
const canImpersonate = currentUser?.canImpersonate;
|
||||
|
||||
const canManageFeatureFlags = useAtomStateValue(canManageFeatureFlagsState);
|
||||
|
||||
const handleSearch = async () => {
|
||||
setActiveTabId('');
|
||||
setIsUserLookupLoading(true);
|
||||
setUserLookupResult(null);
|
||||
const { data: recentUsersData, loading: isLoadingUsers } = useQuery<{
|
||||
adminPanelRecentUsers: {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
createdAt: string;
|
||||
workspaceName?: string | null;
|
||||
workspaceId?: string | null;
|
||||
}[];
|
||||
}>(ADMIN_PANEL_RECENT_USERS, {
|
||||
variables: { searchTerm: debouncedUserSearchTerm },
|
||||
skip: !canImpersonate,
|
||||
});
|
||||
|
||||
const response = await userLookup({
|
||||
variables: { userIdentifier },
|
||||
onCompleted: (data) => {
|
||||
setIsUserLookupLoading(false);
|
||||
if (isDefined(data?.userLookupAdminPanel)) {
|
||||
setUserLookupResult(data.userLookupAdminPanel);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsUserLookupLoading(false);
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: error,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { data: topWorkspacesData, loading: isLoadingWorkspaces } = useQuery<{
|
||||
adminPanelTopWorkspaces: {
|
||||
id: string;
|
||||
name: string;
|
||||
totalUsers: number;
|
||||
subdomain: string;
|
||||
}[];
|
||||
}>(ADMIN_PANEL_TOP_WORKSPACES, {
|
||||
variables: { searchTerm: debouncedWorkspaceSearchTerm },
|
||||
skip: !canImpersonate,
|
||||
});
|
||||
|
||||
const result = response.data?.userLookupAdminPanel;
|
||||
|
||||
if (isDefined(result?.workspaces) && result.workspaces.length > 0) {
|
||||
setActiveTabId(result.workspaces[0].id);
|
||||
}
|
||||
};
|
||||
|
||||
const activeWorkspace = userLookupResult?.workspaces.find(
|
||||
(workspace) => workspace.id === activeTabId,
|
||||
);
|
||||
|
||||
const tabs =
|
||||
userLookupResult?.workspaces.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
title: workspace.name,
|
||||
logo:
|
||||
getImageAbsoluteURI({
|
||||
imageUrl: isNonEmptyString(workspace.logo)
|
||||
? workspace.logo
|
||||
: DEFAULT_WORKSPACE_LOGO,
|
||||
baseUrl: REACT_APP_SERVER_BASE_URL,
|
||||
}) ?? '',
|
||||
})) ?? [];
|
||||
|
||||
const userFullName = `${userLookupResult?.user.firstName || ''} ${
|
||||
userLookupResult?.user.lastName || ''
|
||||
}`.trim();
|
||||
|
||||
const userInfoItems = [
|
||||
{
|
||||
Icon: IconUser,
|
||||
label: t`Name`,
|
||||
value: userFullName,
|
||||
},
|
||||
{
|
||||
Icon: IconMail,
|
||||
label: t`Email`,
|
||||
value: userLookupResult?.user.email,
|
||||
},
|
||||
{
|
||||
Icon: IconId,
|
||||
label: t`ID`,
|
||||
value: userLookupResult?.user.id,
|
||||
},
|
||||
];
|
||||
const recentUsers = recentUsersData?.adminPanelRecentUsers ?? [];
|
||||
const topWorkspaces = topWorkspacesData?.adminPanelTopWorkspaces ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -143,63 +82,105 @@ export const SettingsAdminGeneral = () => {
|
||||
)}
|
||||
|
||||
{canImpersonate && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={
|
||||
canManageFeatureFlags
|
||||
? t`Feature Flags & Impersonation`
|
||||
: t`User Impersonation`
|
||||
}
|
||||
description={
|
||||
canManageFeatureFlags
|
||||
? t`Look up users and manage their workspace feature flags or impersonate them.`
|
||||
: t`Look up users to impersonate them.`
|
||||
}
|
||||
/>
|
||||
|
||||
<StyledContainer>
|
||||
<SettingsTextInput
|
||||
instanceId="admin-user-lookup"
|
||||
value={userIdentifier}
|
||||
onChange={setUserIdentifier}
|
||||
onInputEnter={handleSearch}
|
||||
placeholder={t`Enter user ID or email address`}
|
||||
fullWidth
|
||||
disabled={isUserLookupLoading}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconSearch}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
title={t`Search`}
|
||||
onClick={handleSearch}
|
||||
disabled={!userIdentifier.trim() || isUserLookupLoading}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{isDefined(userLookupResult) && (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title title={t`User Info`} description={t`About this user`} />
|
||||
<SettingsTableCard
|
||||
items={userInfoItems}
|
||||
rounded
|
||||
gridAutoColumns="1fr 4fr"
|
||||
<H2Title
|
||||
title={t`Recent Users`}
|
||||
description={
|
||||
canManageFeatureFlags
|
||||
? t`Last 10 users created. Click to manage feature flags or impersonate.`
|
||||
: t`Last 10 users created. Click to impersonate.`
|
||||
}
|
||||
/>
|
||||
<SettingsTextInput
|
||||
instanceId="admin-panel-user-search"
|
||||
value={userSearchTerm}
|
||||
onChange={setUserSearchTerm}
|
||||
placeholder={t`Search by name, email, or user ID...`}
|
||||
fullWidth
|
||||
/>
|
||||
{recentUsers.length === 0 ? (
|
||||
<StyledEmptyState>
|
||||
{isLoadingUsers
|
||||
? t`Loading...`
|
||||
: t`No users found matching your search criteria.`}
|
||||
</StyledEmptyState>
|
||||
) : (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow gridTemplateColumns="1fr 2fr 1fr">
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Email`}</TableHeader>
|
||||
<TableHeader align="right">{t`Workspace`}</TableHeader>
|
||||
</TableRow>
|
||||
{recentUsers.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
gridTemplateColumns="1fr 2fr 1fr"
|
||||
to={getSettingsPath(SettingsPath.AdminPanelUserDetail, {
|
||||
userId: user.id,
|
||||
})}
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
{`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||
'\u2014'}
|
||||
</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell align="right">
|
||||
{user.workspaceName || '\u2014'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Workspaces`}
|
||||
description={t`All workspaces this user is a member of`}
|
||||
title={t`Top Workspaces`}
|
||||
description={t`Top 10 workspaces by number of users`}
|
||||
/>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID}
|
||||
<SettingsTextInput
|
||||
instanceId="admin-panel-workspace-search"
|
||||
value={workspaceSearchTerm}
|
||||
onChange={setWorkspaceSearchTerm}
|
||||
placeholder={t`Search by workspace name, subdomain, or ID...`}
|
||||
fullWidth
|
||||
/>
|
||||
<SettingsAdminWorkspaceContent activeWorkspace={activeWorkspace} />
|
||||
{topWorkspaces.length === 0 ? (
|
||||
<StyledEmptyState>
|
||||
{isLoadingWorkspaces
|
||||
? t`Loading...`
|
||||
: t`No workspaces found matching your search criteria.`}
|
||||
</StyledEmptyState>
|
||||
) : (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow gridTemplateColumns="2fr 1fr">
|
||||
<TableHeader>{t`Workspace`}</TableHeader>
|
||||
<TableHeader align="right">{t`Users`}</TableHeader>
|
||||
</TableRow>
|
||||
{topWorkspaces.map((workspace) => (
|
||||
<TableRow
|
||||
key={workspace.id}
|
||||
gridTemplateColumns="2fr 1fr"
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AdminPanelWorkspaceDetail,
|
||||
{ workspaceId: workspace.id },
|
||||
)}
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
{workspace.name || '\u2014'}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{workspace.totalUsers}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
)}
|
||||
|
||||
+28
-165
@@ -1,46 +1,29 @@
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState';
|
||||
import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth';
|
||||
import { useImpersonationRedirect } from '@/settings/admin-panel/hooks/useImpersonationRedirect';
|
||||
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
|
||||
import { type WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
|
||||
import { getWorkspaceSchemaName } from '@/settings/admin-panel/utils/getWorkspaceSchemaName';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils';
|
||||
import { AvatarOrIcon, Chip } from 'twenty-ui/components';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
getImageAbsoluteURI,
|
||||
getSettingsPath,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { AvatarOrIcon, LinkChip } from 'twenty-ui/components';
|
||||
import {
|
||||
H2Title,
|
||||
IconEyeShare,
|
||||
IconCalendar,
|
||||
IconHome,
|
||||
IconId,
|
||||
IconLink,
|
||||
IconStatusChange,
|
||||
IconUser,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button, Toggle } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import {
|
||||
type FeatureFlagKey,
|
||||
ImpersonateDocument,
|
||||
UpdateWorkspaceFeatureFlagDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsAdminWorkspaceContentProps = {
|
||||
activeWorkspace: WorkspaceInfo | undefined;
|
||||
@@ -53,93 +36,11 @@ const StyledContainer = styled.div`
|
||||
margin-top: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminWorkspaceContent = ({
|
||||
activeWorkspace,
|
||||
}: SettingsAdminWorkspaceContentProps) => {
|
||||
const canManageFeatureFlags = useAtomStateValue(canManageFeatureFlagsState);
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const [currentUser] = useAtomState(currentUserState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
||||
const [updateFeatureFlag] = useMutation(UpdateWorkspaceFeatureFlagDocument);
|
||||
const [isImpersonateLoading, setIsImpersonationLoading] = useState(false);
|
||||
const { executeImpersonationAuth } = useImpersonationAuth();
|
||||
const { executeImpersonationRedirect } = useImpersonationRedirect();
|
||||
const [impersonate] = useMutation(ImpersonateDocument);
|
||||
|
||||
const { updateFeatureFlagState } = useFeatureFlagState();
|
||||
const userLookupResult = useAtomStateValue(userLookupResultState);
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const handleImpersonate = async (workspaceId: string) => {
|
||||
if (!userLookupResult?.user.id) {
|
||||
enqueueErrorSnackBar({ message: t`Please search for a user first` });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsImpersonationLoading(true);
|
||||
|
||||
await impersonate({
|
||||
variables: { userId: userLookupResult.user.id, workspaceId },
|
||||
onCompleted: async (data) => {
|
||||
const { loginToken, workspace } = data.impersonate;
|
||||
const isCurrentWorkspace = workspace.id === currentWorkspace?.id;
|
||||
if (isCurrentWorkspace) {
|
||||
await executeImpersonationAuth(loginToken.token);
|
||||
return;
|
||||
}
|
||||
|
||||
return executeImpersonationRedirect(
|
||||
workspace.workspaceUrls,
|
||||
loginToken.token,
|
||||
'_blank',
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error.message;
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to impersonate user. ${errorMessage}`,
|
||||
});
|
||||
},
|
||||
}).finally(() => {
|
||||
setIsImpersonationLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFeatureFlagUpdate = async (
|
||||
workspaceId: string,
|
||||
featureFlag: FeatureFlagKey,
|
||||
value: boolean,
|
||||
) => {
|
||||
const previousValue = userLookupResult?.workspaces
|
||||
.find((workspace) => workspace.id === workspaceId)
|
||||
?.featureFlags.find((flag) => flag.key === featureFlag)?.value;
|
||||
|
||||
updateFeatureFlagState(workspaceId, featureFlag, value);
|
||||
await updateFeatureFlag({
|
||||
variables: {
|
||||
workspaceId,
|
||||
featureFlag,
|
||||
value,
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
if (isDefined(previousValue)) {
|
||||
updateFeatureFlagState(workspaceId, featureFlag, previousValue);
|
||||
}
|
||||
const errorMessage = error.message;
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to update feature flag. ${errorMessage}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getWorkspaceUrl = (workspaceUrls: WorkspaceInfo['workspaceUrls']) => {
|
||||
return workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl;
|
||||
};
|
||||
@@ -148,10 +49,13 @@ export const SettingsAdminWorkspaceContent = ({
|
||||
{
|
||||
Icon: IconHome,
|
||||
label: t`Name`,
|
||||
value: (
|
||||
<Chip
|
||||
value: activeWorkspace?.id ? (
|
||||
<LinkChip
|
||||
label={activeWorkspace?.name ?? ''}
|
||||
emptyLabel={t`Untitled`}
|
||||
to={getSettingsPath(SettingsPath.AdminPanelWorkspaceDetail, {
|
||||
workspaceId: activeWorkspace.id,
|
||||
})}
|
||||
leftComponent={
|
||||
<AvatarOrIcon
|
||||
avatarUrl={
|
||||
@@ -165,6 +69,8 @@ export const SettingsAdminWorkspaceContent = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
(activeWorkspace?.name ?? '')
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -191,6 +97,18 @@ export const SettingsAdminWorkspaceContent = ({
|
||||
label: t`Members`,
|
||||
value: activeWorkspace?.totalUsers,
|
||||
},
|
||||
{
|
||||
Icon: IconStatusChange,
|
||||
label: t`Status`,
|
||||
value: activeWorkspace?.activationStatus,
|
||||
},
|
||||
{
|
||||
Icon: IconCalendar,
|
||||
label: t`Created`,
|
||||
value: activeWorkspace?.createdAt
|
||||
? new Date(activeWorkspace.createdAt).toLocaleDateString()
|
||||
: '',
|
||||
},
|
||||
];
|
||||
|
||||
if (!activeWorkspace) return null;
|
||||
@@ -206,62 +124,7 @@ export const SettingsAdminWorkspaceContent = ({
|
||||
items={workspaceInfoItems}
|
||||
gridAutoColumns="1fr 4fr"
|
||||
/>
|
||||
<StyledButtonContainer>
|
||||
{currentUser?.canImpersonate && (
|
||||
<Button
|
||||
Icon={IconEyeShare}
|
||||
variant="primary"
|
||||
accent="default"
|
||||
title={
|
||||
activeWorkspace.allowImpersonation === false
|
||||
? t`Impersonation is disabled for this workspace`
|
||||
: t`Impersonate`
|
||||
}
|
||||
onClick={() => handleImpersonate(activeWorkspace.id)}
|
||||
disabled={
|
||||
isImpersonateLoading ||
|
||||
activeWorkspace.allowImpersonation === false
|
||||
}
|
||||
dataTestId="impersonate-button"
|
||||
/>
|
||||
)}
|
||||
</StyledButtonContainer>
|
||||
</Section>
|
||||
{canManageFeatureFlags && (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
gridAutoColumns="1fr 100px"
|
||||
mobileGridAutoColumns="1fr 80px"
|
||||
>
|
||||
<TableHeader>{t`Feature Flag`}</TableHeader>
|
||||
<TableHeader align="right">{t`Status`}</TableHeader>
|
||||
</TableRow>
|
||||
|
||||
{activeWorkspace.featureFlags.map((flag) => (
|
||||
<TableRow
|
||||
gridAutoColumns="1fr 100px"
|
||||
mobileGridAutoColumns="1fr 80px"
|
||||
key={flag.key}
|
||||
>
|
||||
<TableCell>{flag.key}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Toggle
|
||||
value={flag.value}
|
||||
onChange={(newValue) =>
|
||||
handleFeatureFlagUpdate(
|
||||
activeWorkspace.id,
|
||||
flag.key,
|
||||
newValue,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+3
@@ -8,12 +8,15 @@ export const USER_LOOKUP_ADMIN_PANEL = gql`
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
createdAt
|
||||
}
|
||||
workspaces {
|
||||
id
|
||||
name
|
||||
logo
|
||||
totalUsers
|
||||
activationStatus
|
||||
createdAt
|
||||
allowImpersonation
|
||||
workspaceUrls {
|
||||
customUrl
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const ADMIN_PANEL_RECENT_USERS = gql`
|
||||
query AdminPanelRecentUsers($searchTerm: String) {
|
||||
adminPanelRecentUsers(searchTerm: $searchTerm) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
createdAt
|
||||
workspaceName
|
||||
workspaceId
|
||||
}
|
||||
}
|
||||
`;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const ADMIN_PANEL_TOP_WORKSPACES = gql`
|
||||
query AdminPanelTopWorkspaces($searchTerm: String) {
|
||||
adminPanelTopWorkspaces(searchTerm: $searchTerm) {
|
||||
id
|
||||
name
|
||||
totalUsers
|
||||
subdomain
|
||||
}
|
||||
}
|
||||
`;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_ADMIN_CHAT_THREAD_MESSAGES = gql`
|
||||
query GetAdminChatThreadMessages($workspaceId: String!, $threadId: String!) {
|
||||
getAdminChatThreadMessages(workspaceId: $workspaceId, threadId: $threadId) {
|
||||
thread {
|
||||
id
|
||||
title
|
||||
totalInputTokens
|
||||
totalOutputTokens
|
||||
conversationSize
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
messages {
|
||||
id
|
||||
role
|
||||
parts {
|
||||
type
|
||||
textContent
|
||||
toolName
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_ADMIN_WORKSPACE_CHAT_THREADS = gql`
|
||||
query GetAdminWorkspaceChatThreads($workspaceId: String!) {
|
||||
getAdminWorkspaceChatThreads(workspaceId: $workspaceId) {
|
||||
id
|
||||
title
|
||||
totalInputTokens
|
||||
totalOutputTokens
|
||||
conversationSize
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const WORKSPACE_LOOKUP_ADMIN_PANEL = gql`
|
||||
query WorkspaceLookupAdminPanel($workspaceId: String!) {
|
||||
workspaceLookupAdminPanel(workspaceId: $workspaceId) {
|
||||
user {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
createdAt
|
||||
}
|
||||
workspaces {
|
||||
id
|
||||
name
|
||||
allowImpersonation
|
||||
logo
|
||||
totalUsers
|
||||
activationStatus
|
||||
createdAt
|
||||
workspaceUrls {
|
||||
customUrl
|
||||
subdomainUrl
|
||||
}
|
||||
users {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
featureFlags {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useClearSseClient } from '@/sse-db-event/hooks/useClearSseClient';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export const useImpersonationAuth = () => {
|
||||
const { getAuthTokensFromLoginToken } = useAuth();
|
||||
const { clearSseClient } = useClearSseClient();
|
||||
const setIsAppEffectRedirectEnabled = useSetAtomState(
|
||||
isAppEffectRedirectEnabledState,
|
||||
);
|
||||
|
||||
const executeImpersonationAuth = async (loginToken: string) => {
|
||||
setIsAppEffectRedirectEnabled(false);
|
||||
clearSseClient();
|
||||
await getAuthTokensFromLoginToken(loginToken);
|
||||
setIsAppEffectRedirectEnabled(true);
|
||||
};
|
||||
|
||||
return { executeImpersonationAuth };
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export type AdminChatThread = {
|
||||
id: string;
|
||||
title: string | null;
|
||||
totalInputTokens: number;
|
||||
totalOutputTokens: number;
|
||||
conversationSize: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ export type UserLookup = {
|
||||
email: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
workspaces: WorkspaceInfo[];
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ export type WorkspaceInfo = {
|
||||
name: string;
|
||||
logo?: string | null;
|
||||
totalUsers: number;
|
||||
activationStatus: string;
|
||||
createdAt: string;
|
||||
workspaceUrls: WorkspaceUrls;
|
||||
users: {
|
||||
id: string;
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
getImageAbsoluteURI,
|
||||
getSettingsPath,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent';
|
||||
import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId';
|
||||
import { useImpersonationSession } from '@/auth/hooks/useImpersonationSession';
|
||||
import { useImpersonationRedirect } from '@/settings/admin-panel/hooks/useImpersonationRedirect';
|
||||
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import {
|
||||
H2Title,
|
||||
IconCalendar,
|
||||
IconEyeShare,
|
||||
IconId,
|
||||
IconMail,
|
||||
IconUser,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import {
|
||||
ImpersonateDocument,
|
||||
UserLookupAdminPanelDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminUserDetail = () => {
|
||||
const { userId } = useParams<{ userId: string }>();
|
||||
|
||||
const [activeTabId, setActiveTabId] = useAtomComponentState(
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID,
|
||||
);
|
||||
|
||||
const [userLookupResult, setUserLookupResult] = useAtomState(
|
||||
userLookupResultState,
|
||||
);
|
||||
|
||||
const [userLookup, { loading: isLoading }] = useMutation(
|
||||
UserLookupAdminPanelDocument,
|
||||
);
|
||||
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { startImpersonating } = useImpersonationSession();
|
||||
const { executeImpersonationRedirect } = useImpersonationRedirect();
|
||||
const [impersonate] = useMutation(ImpersonateDocument);
|
||||
const [isImpersonateLoading, setIsImpersonateLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
userLookup({
|
||||
variables: { userIdentifier: userId },
|
||||
onCompleted: (data) => {
|
||||
if (isDefined(data?.userLookupAdminPanel)) {
|
||||
setUserLookupResult(data.userLookupAdminPanel);
|
||||
|
||||
if (data.userLookupAdminPanel.workspaces.length > 0 && !activeTabId) {
|
||||
setActiveTabId(data.userLookupAdminPanel.workspaces[0].id);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [userId, userLookup, setUserLookupResult, setActiveTabId, activeTabId]);
|
||||
|
||||
const handleImpersonate = async (workspaceId: string) => {
|
||||
if (!userLookupResult?.user.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsImpersonateLoading(true);
|
||||
|
||||
await impersonate({
|
||||
variables: { userId: userLookupResult.user.id, workspaceId },
|
||||
onCompleted: async (data) => {
|
||||
const { loginToken, workspace } = data.impersonate;
|
||||
const isCurrentWorkspace = workspace.id === currentWorkspace?.id;
|
||||
|
||||
if (isCurrentWorkspace) {
|
||||
await startImpersonating(loginToken.token);
|
||||
return;
|
||||
}
|
||||
|
||||
return executeImpersonationRedirect(
|
||||
workspace.workspaceUrls,
|
||||
loginToken.token,
|
||||
'_blank',
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueErrorSnackBar({
|
||||
message: `Failed to impersonate user. ${error.message}`,
|
||||
});
|
||||
},
|
||||
}).finally(() => {
|
||||
setIsImpersonateLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const userFullName = `${userLookupResult?.user.firstName || ''} ${
|
||||
userLookupResult?.user.lastName || ''
|
||||
}`.trim();
|
||||
|
||||
const activeWorkspace = userLookupResult?.workspaces.find(
|
||||
(workspace) => workspace.id === activeTabId,
|
||||
);
|
||||
|
||||
const tabs =
|
||||
userLookupResult?.workspaces.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
title: workspace.name,
|
||||
logo:
|
||||
getImageAbsoluteURI({
|
||||
imageUrl: isNonEmptyString(workspace.logo)
|
||||
? workspace.logo
|
||||
: DEFAULT_WORKSPACE_LOGO,
|
||||
baseUrl: REACT_APP_SERVER_BASE_URL,
|
||||
}) ?? '',
|
||||
})) ?? [];
|
||||
|
||||
const userInfoItems = [
|
||||
{
|
||||
Icon: IconUser,
|
||||
label: t`Name`,
|
||||
value: userFullName,
|
||||
},
|
||||
{
|
||||
Icon: IconMail,
|
||||
label: t`Email`,
|
||||
value: userLookupResult?.user.email,
|
||||
},
|
||||
{
|
||||
Icon: IconId,
|
||||
label: t`ID`,
|
||||
value: userLookupResult?.user.id,
|
||||
},
|
||||
{
|
||||
Icon: IconCalendar,
|
||||
label: t`Created`,
|
||||
value: userLookupResult?.user.createdAt
|
||||
? new Date(userLookupResult.user.createdAt).toLocaleDateString()
|
||||
: '',
|
||||
},
|
||||
];
|
||||
|
||||
const displayName = userFullName || userId || '';
|
||||
|
||||
if (isLoading && !userLookupResult) {
|
||||
return <SettingsSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
links={[
|
||||
{
|
||||
children: t`Other`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel - General`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: displayName,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
{isDefined(userLookupResult) && (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title title={t`User Info`} description={t`About this user`} />
|
||||
<SettingsTableCard
|
||||
items={userInfoItems}
|
||||
rounded
|
||||
gridAutoColumns="1fr 4fr"
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Workspaces`}
|
||||
description={t`All workspaces this user is a member of`}
|
||||
/>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={
|
||||
SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID
|
||||
}
|
||||
/>
|
||||
<SettingsAdminWorkspaceContent
|
||||
activeWorkspace={activeWorkspace}
|
||||
/>
|
||||
{currentUser?.canImpersonate && activeWorkspace && (
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconEyeShare}
|
||||
variant="primary"
|
||||
accent="default"
|
||||
title={
|
||||
activeWorkspace.allowImpersonation === false
|
||||
? t`Impersonation is disabled for this workspace`
|
||||
: t`Impersonate`
|
||||
}
|
||||
onClick={() => handleImpersonate(activeWorkspace.id)}
|
||||
disabled={
|
||||
isImpersonateLoading ||
|
||||
activeWorkspace.allowImpersonation === false
|
||||
}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
+404
@@ -0,0 +1,404 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
|
||||
import { AI_ADMIN_PATH } from '@/settings/admin-panel/ai/constants/AiAdminPath';
|
||||
import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent';
|
||||
import { GET_ADMIN_WORKSPACE_CHAT_THREADS } from '@/settings/admin-panel/graphql/queries/getAdminWorkspaceChatThreads';
|
||||
import { WORKSPACE_LOOKUP_ADMIN_PANEL } from '@/settings/admin-panel/graphql/queries/workspaceLookupAdminPanel';
|
||||
import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState';
|
||||
import { useImpersonationSession } from '@/auth/hooks/useImpersonationSession';
|
||||
import { useImpersonationRedirect } from '@/settings/admin-panel/hooks/useImpersonationRedirect';
|
||||
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
|
||||
import { type AdminChatThread } from '@/settings/admin-panel/types/AdminChatThread';
|
||||
import { type UserLookup } from '@/settings/admin-panel/types/UserLookup';
|
||||
import { type WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import {
|
||||
H2Title,
|
||||
IconEyeShare,
|
||||
IconFlag,
|
||||
IconMessage,
|
||||
IconSettings2,
|
||||
IconUsers,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button, Toggle } from 'twenty-ui/input';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
type FeatureFlagKey,
|
||||
ImpersonateDocument,
|
||||
UpdateWorkspaceFeatureFlagDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const WORKSPACE_DETAIL_TABS_ID = 'settings-admin-workspace-detail-tabs';
|
||||
|
||||
const WORKSPACE_DETAIL_TAB_IDS = {
|
||||
INFO: 'info',
|
||||
MEMBERS: 'members',
|
||||
FEATURE_FLAGS: 'feature-flags',
|
||||
CHATS: 'chats',
|
||||
};
|
||||
|
||||
export const SettingsAdminWorkspaceDetail = () => {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
|
||||
const [activeTabId] = useAtomComponentState(
|
||||
activeTabIdComponentState,
|
||||
WORKSPACE_DETAIL_TABS_ID,
|
||||
);
|
||||
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const canManageFeatureFlags = useAtomStateValue(canManageFeatureFlagsState);
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const [updateFeatureFlag] = useMutation(UpdateWorkspaceFeatureFlagDocument);
|
||||
const { updateFeatureFlagState } = useFeatureFlagState();
|
||||
const { startImpersonating } = useImpersonationSession();
|
||||
const { executeImpersonationRedirect } = useImpersonationRedirect();
|
||||
const [impersonate] = useMutation(ImpersonateDocument);
|
||||
const [impersonatingUserId, setImpersonatingUserId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [, setUserLookupResult] = useAtomState(userLookupResultState);
|
||||
const userLookupResult = useAtomStateValue(userLookupResultState);
|
||||
|
||||
const { data: workspaceData, loading: isLoadingWorkspace } = useQuery<{
|
||||
workspaceLookupAdminPanel: UserLookup;
|
||||
}>(WORKSPACE_LOOKUP_ADMIN_PANEL, {
|
||||
variables: { workspaceId },
|
||||
skip: !workspaceId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(workspaceData?.workspaceLookupAdminPanel)) {
|
||||
setUserLookupResult(workspaceData.workspaceLookupAdminPanel);
|
||||
}
|
||||
}, [workspaceData, setUserLookupResult]);
|
||||
|
||||
const workspace = workspaceData?.workspaceLookupAdminPanel
|
||||
?.workspaces?.[0] as WorkspaceInfo | undefined;
|
||||
|
||||
const effectiveTabId = activeTabId || WORKSPACE_DETAIL_TAB_IDS.INFO;
|
||||
|
||||
const { data: threadsData, loading: isLoadingThreads } = useQuery<{
|
||||
getAdminWorkspaceChatThreads: AdminChatThread[];
|
||||
}>(GET_ADMIN_WORKSPACE_CHAT_THREADS, {
|
||||
variables: { workspaceId },
|
||||
skip:
|
||||
!workspaceId ||
|
||||
!workspace?.allowImpersonation ||
|
||||
effectiveTabId !== WORKSPACE_DETAIL_TAB_IDS.CHATS,
|
||||
});
|
||||
|
||||
const threads = threadsData?.getAdminWorkspaceChatThreads ?? [];
|
||||
|
||||
const handleImpersonate = async (userId: string) => {
|
||||
if (!workspaceId) return;
|
||||
|
||||
setImpersonatingUserId(userId);
|
||||
|
||||
await impersonate({
|
||||
variables: { userId, workspaceId },
|
||||
onCompleted: async (data) => {
|
||||
const { loginToken, workspace: impersonatedWorkspace } =
|
||||
data.impersonate;
|
||||
const isCurrentWorkspace =
|
||||
impersonatedWorkspace.id === currentWorkspace?.id;
|
||||
|
||||
if (isCurrentWorkspace) {
|
||||
await startImpersonating(loginToken.token);
|
||||
return;
|
||||
}
|
||||
|
||||
return executeImpersonationRedirect(
|
||||
impersonatedWorkspace.workspaceUrls,
|
||||
loginToken.token,
|
||||
'_blank',
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueErrorSnackBar({
|
||||
message: `Failed to impersonate user. ${error.message}`,
|
||||
});
|
||||
},
|
||||
}).finally(() => {
|
||||
setImpersonatingUserId(null);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFeatureFlagUpdate = async (
|
||||
featureFlag: FeatureFlagKey,
|
||||
value: boolean,
|
||||
) => {
|
||||
if (!workspaceId) return;
|
||||
|
||||
const previousValue = userLookupResult?.workspaces
|
||||
.find((ws) => ws.id === workspaceId)
|
||||
?.featureFlags.find((flag) => flag.key === featureFlag)?.value;
|
||||
|
||||
updateFeatureFlagState(workspaceId, featureFlag, value);
|
||||
await updateFeatureFlag({
|
||||
variables: {
|
||||
workspaceId,
|
||||
featureFlag,
|
||||
value,
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isDefined(previousValue)) {
|
||||
updateFeatureFlagState(workspaceId, featureFlag, previousValue);
|
||||
}
|
||||
enqueueErrorSnackBar({
|
||||
message: `Failed to update feature flag. ${error.message}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
const result = [
|
||||
{
|
||||
id: WORKSPACE_DETAIL_TAB_IDS.INFO,
|
||||
title: t`Info`,
|
||||
Icon: IconSettings2,
|
||||
},
|
||||
];
|
||||
|
||||
if (currentUser?.canImpersonate) {
|
||||
result.push({
|
||||
id: WORKSPACE_DETAIL_TAB_IDS.MEMBERS,
|
||||
title: t`Members`,
|
||||
Icon: IconUsers,
|
||||
});
|
||||
}
|
||||
|
||||
if (canManageFeatureFlags) {
|
||||
result.push({
|
||||
id: WORKSPACE_DETAIL_TAB_IDS.FEATURE_FLAGS,
|
||||
title: t`Feature Flags`,
|
||||
Icon: IconFlag,
|
||||
});
|
||||
}
|
||||
|
||||
if (workspace?.allowImpersonation) {
|
||||
result.push({
|
||||
id: WORKSPACE_DETAIL_TAB_IDS.CHATS,
|
||||
title: t`Chats`,
|
||||
Icon: IconMessage,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [
|
||||
workspace?.allowImpersonation,
|
||||
canManageFeatureFlags,
|
||||
currentUser?.canImpersonate,
|
||||
]);
|
||||
|
||||
const workspaceName = workspace?.name || workspaceId || '';
|
||||
|
||||
if (isLoadingWorkspace) {
|
||||
return <SettingsSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
links={[
|
||||
{
|
||||
children: t`Other`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`AI`,
|
||||
href: AI_ADMIN_PATH,
|
||||
},
|
||||
{
|
||||
children: workspaceName,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={WORKSPACE_DETAIL_TABS_ID}
|
||||
/>
|
||||
|
||||
{effectiveTabId === WORKSPACE_DETAIL_TAB_IDS.INFO && workspace && (
|
||||
<SettingsAdminWorkspaceContent activeWorkspace={workspace} />
|
||||
)}
|
||||
|
||||
{effectiveTabId === WORKSPACE_DETAIL_TAB_IDS.MEMBERS && workspace && (
|
||||
<Section>
|
||||
<H2Title title={t`Members`} description={t`Workspace members`} />
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow gridTemplateColumns="1fr 2fr 100px">
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Email`}</TableHeader>
|
||||
<TableHeader align="right">{t`Actions`}</TableHeader>
|
||||
</TableRow>
|
||||
{workspace.users?.map((user) => {
|
||||
const userId = user.id;
|
||||
|
||||
if (!isDefined(userId)) return null;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={userId}
|
||||
gridTemplateColumns="1fr 2fr 100px"
|
||||
to={getSettingsPath(SettingsPath.AdminPanelUserDetail, {
|
||||
userId,
|
||||
})}
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
{`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||
'\u2014'}
|
||||
</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell align="right">
|
||||
{workspace.allowImpersonation && (
|
||||
<Button
|
||||
Icon={IconEyeShare}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
title={t`Impersonate`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleImpersonate(userId);
|
||||
}}
|
||||
disabled={impersonatingUserId === userId}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{effectiveTabId === WORKSPACE_DETAIL_TAB_IDS.FEATURE_FLAGS &&
|
||||
workspace && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Feature Flags`}
|
||||
description={t`Manage feature flags for this workspace`}
|
||||
/>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
gridAutoColumns="1fr 100px"
|
||||
mobileGridAutoColumns="1fr 80px"
|
||||
>
|
||||
<TableHeader>{t`Feature Flag`}</TableHeader>
|
||||
<TableHeader align="right">{t`Status`}</TableHeader>
|
||||
</TableRow>
|
||||
{workspace.featureFlags?.map((flag) => (
|
||||
<TableRow
|
||||
gridAutoColumns="1fr 100px"
|
||||
mobileGridAutoColumns="1fr 80px"
|
||||
key={flag.key}
|
||||
>
|
||||
<TableCell>{flag.key}</TableCell>
|
||||
<TableCell align="right">
|
||||
{isDefined(flag.key) && (
|
||||
<Toggle
|
||||
value={flag.value}
|
||||
onChange={(newValue) =>
|
||||
handleFeatureFlagUpdate(flag.key!, newValue)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{effectiveTabId === WORKSPACE_DETAIL_TAB_IDS.CHATS && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Chat Sessions`}
|
||||
description={t`AI chat threads for this workspace`}
|
||||
/>
|
||||
{threads.length === 0 ? (
|
||||
<Card rounded>
|
||||
<TableRow gridTemplateColumns="1fr">
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
align="center"
|
||||
>
|
||||
{isLoadingThreads
|
||||
? t`Loading...`
|
||||
: t`No chat threads found.`}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Card>
|
||||
) : (
|
||||
<Table>
|
||||
<TableRow gridTemplateColumns="1fr 120px 120px">
|
||||
<TableHeader>{t`Title`}</TableHeader>
|
||||
<TableHeader align="right">{t`Messages`}</TableHeader>
|
||||
<TableHeader align="right">{t`Updated`}</TableHeader>
|
||||
</TableRow>
|
||||
{threads.map((thread) => (
|
||||
<TableRow
|
||||
key={thread.id}
|
||||
gridTemplateColumns="1fr 120px 120px"
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AdminPanelWorkspaceChatThread,
|
||||
{
|
||||
workspaceId: workspaceId ?? '',
|
||||
threadId: thread.id,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
{thread.title || t`Untitled`}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{thread.conversationSize}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{new Date(thread.updatedAt).toLocaleDateString()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { CoreObjectNameSingular, SettingsPath } from 'twenty-shared/types';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth';
|
||||
import { useImpersonationSession } from '@/auth/hooks/useImpersonationSession';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
@@ -50,7 +50,7 @@ export const SettingsWorkspaceMember = () => {
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
const { openModal, closeModal } = useModal();
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const { executeImpersonationAuth } = useImpersonationAuth();
|
||||
const { startImpersonating } = useImpersonationSession();
|
||||
const [impersonate] = useMutation(ImpersonateDocument);
|
||||
const isImpersonating = useAtomStateValue(isImpersonatingState);
|
||||
const canImpersonate =
|
||||
@@ -150,7 +150,7 @@ export const SettingsWorkspaceMember = () => {
|
||||
},
|
||||
onCompleted: async (data) => {
|
||||
const { loginToken } = data.impersonate;
|
||||
await executeImpersonationAuth(loginToken.token);
|
||||
await startImpersonating(loginToken.token);
|
||||
},
|
||||
onError: () => {
|
||||
enqueueErrorSnackBar({
|
||||
|
||||
+25
@@ -5,10 +5,15 @@ import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-pan
|
||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
|
||||
|
||||
const UserFindOneMock = jest.fn();
|
||||
const LoginTokenServiceGenerateLoginTokenMock = jest.fn();
|
||||
@@ -53,6 +58,26 @@ describe('AdminPanelService', () => {
|
||||
findOne: UserFindOneMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(UserWorkspaceEntity),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(FeatureFlagEntity),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AgentChatThreadEntity),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AgentMessageEntity),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: LoginTokenService,
|
||||
useValue: {
|
||||
|
||||
@@ -24,15 +24,26 @@ import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-cl
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
|
||||
import { UsageModule } from 'src/engine/core-modules/usage/usage.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
|
||||
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([UserEntity, WorkspaceEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
UserEntity,
|
||||
WorkspaceEntity,
|
||||
UserWorkspaceEntity,
|
||||
FeatureFlagEntity,
|
||||
AgentChatThreadEntity,
|
||||
AgentMessageEntity,
|
||||
]),
|
||||
AuthModule,
|
||||
FileModule,
|
||||
WorkspaceDomainsModule,
|
||||
|
||||
@@ -11,6 +11,10 @@ import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/adm
|
||||
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
|
||||
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
|
||||
import { MaintenanceModeService } from 'src/engine/core-modules/admin-panel/maintenance-mode.service';
|
||||
import { AdminPanelRecentUserDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-recent-user.dto';
|
||||
import { AdminPanelTopWorkspaceDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-top-workspace.dto';
|
||||
import { AdminWorkspaceChatThreadDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-workspace-chat-thread.dto';
|
||||
import { AdminChatThreadMessagesDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-chat-thread-messages.dto';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { AdminAIModelsDTO } from 'src/engine/core-modules/client-config/client-config.entity';
|
||||
@@ -98,6 +102,32 @@ export class AdminPanelResolver {
|
||||
return await this.adminService.userLookup(userLookupInput.userIdentifier);
|
||||
}
|
||||
|
||||
@UseGuards(ServerLevelImpersonateGuard)
|
||||
@Query(() => [AdminPanelRecentUserDTO])
|
||||
async adminPanelRecentUsers(
|
||||
@Args('searchTerm', {
|
||||
type: () => String,
|
||||
nullable: true,
|
||||
defaultValue: '',
|
||||
})
|
||||
searchTerm: string,
|
||||
): Promise<AdminPanelRecentUserDTO[]> {
|
||||
return this.adminService.getRecentUsers(searchTerm);
|
||||
}
|
||||
|
||||
@UseGuards(ServerLevelImpersonateGuard)
|
||||
@Query(() => [AdminPanelTopWorkspaceDTO])
|
||||
async adminPanelTopWorkspaces(
|
||||
@Args('searchTerm', {
|
||||
type: () => String,
|
||||
nullable: true,
|
||||
defaultValue: '',
|
||||
})
|
||||
searchTerm: string,
|
||||
): Promise<AdminPanelTopWorkspaceDTO[]> {
|
||||
return this.adminService.getTopWorkspaces(searchTerm);
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Mutation(() => Boolean)
|
||||
async updateWorkspaceFeatureFlag(
|
||||
@@ -614,4 +644,29 @@ export class AdminPanelResolver {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => UserLookup)
|
||||
async workspaceLookupAdminPanel(
|
||||
@Args('workspaceId', { type: () => String }) workspaceId: string,
|
||||
): Promise<UserLookup> {
|
||||
return this.adminService.workspaceLookup(workspaceId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => [AdminWorkspaceChatThreadDTO])
|
||||
async getAdminWorkspaceChatThreads(
|
||||
@Args('workspaceId', { type: () => String }) workspaceId: string,
|
||||
): Promise<AdminWorkspaceChatThreadDTO[]> {
|
||||
return this.adminService.getWorkspaceChatThreads(workspaceId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => AdminChatThreadMessagesDTO)
|
||||
async getAdminChatThreadMessages(
|
||||
@Args('workspaceId', { type: () => String }) workspaceId: string,
|
||||
@Args('threadId', { type: () => String }) threadId: string,
|
||||
): Promise<AdminChatThreadMessagesDTO> {
|
||||
return this.adminService.getChatThreadMessages(workspaceId, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { type AdminChatMessageDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-chat-message.dto';
|
||||
import { type AdminPanelRecentUserDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-recent-user.dto';
|
||||
import { type AdminPanelTopWorkspaceDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-top-workspace.dto';
|
||||
import { type AdminWorkspaceChatThreadDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-workspace-chat-thread.dto';
|
||||
import { type ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
|
||||
import { type ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
|
||||
import { type ConfigVariablesDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables.dto';
|
||||
@@ -18,15 +22,20 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { type FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||
import { CONFIG_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata';
|
||||
import { type ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AdminPanelService {
|
||||
@@ -37,6 +46,16 @@ export class AdminPanelService {
|
||||
private readonly secureHttpClientService: SecureHttpClientService,
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userRepository: Repository<UserEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
@InjectRepository(FeatureFlagEntity)
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
@InjectRepository(AgentChatThreadEntity)
|
||||
private readonly agentChatThreadRepository: Repository<AgentChatThreadEntity>,
|
||||
@InjectRepository(AgentMessageEntity)
|
||||
private readonly agentMessageRepository: Repository<AgentMessageEntity>,
|
||||
) {}
|
||||
|
||||
async userLookup(userIdentifier: string): Promise<UserLookup> {
|
||||
@@ -76,11 +95,14 @@ export class AdminPanelService {
|
||||
email: targetUser.email,
|
||||
firstName: targetUser.firstName,
|
||||
lastName: targetUser.lastName,
|
||||
createdAt: targetUser.createdAt,
|
||||
},
|
||||
workspaces: targetUser.userWorkspaces.map((userWorkspace) => ({
|
||||
id: userWorkspace.workspace.id,
|
||||
name: userWorkspace.workspace.displayName ?? '',
|
||||
totalUsers: userWorkspace.workspace.workspaceUsers.length,
|
||||
activationStatus: userWorkspace.workspace.activationStatus,
|
||||
createdAt: userWorkspace.workspace.createdAt,
|
||||
logo: isDefined(userWorkspace.workspace.logoFileId)
|
||||
? this.fileUrlService.signFileByIdUrl({
|
||||
fileId: userWorkspace.workspace.logoFileId,
|
||||
@@ -101,6 +123,7 @@ export class AdminPanelService {
|
||||
email: workspaceUser.user.email,
|
||||
firstName: workspaceUser.user.firstName,
|
||||
lastName: workspaceUser.user.lastName,
|
||||
createdAt: workspaceUser.user.createdAt,
|
||||
})),
|
||||
featureFlags: allFeatureFlagKeys.map((key) => ({
|
||||
key,
|
||||
@@ -113,6 +136,253 @@ export class AdminPanelService {
|
||||
};
|
||||
}
|
||||
|
||||
async workspaceLookup(workspaceId: string): Promise<UserLookup> {
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: msg`Workspace not found. Please check the ID.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const [workspaceUsers, featureFlags] = await Promise.all([
|
||||
this.userWorkspaceRepository.find({
|
||||
where: { workspaceId },
|
||||
relations: { user: true },
|
||||
}),
|
||||
this.featureFlagRepository.find({
|
||||
where: { workspaceId },
|
||||
}),
|
||||
]);
|
||||
|
||||
const allFeatureFlagKeys = Object.values(FeatureFlagKey);
|
||||
|
||||
const workspaceInfo = {
|
||||
id: workspace.id,
|
||||
name: workspace.displayName ?? '',
|
||||
totalUsers: workspaceUsers.length,
|
||||
activationStatus: workspace.activationStatus,
|
||||
createdAt: workspace.createdAt,
|
||||
logo: isDefined(workspace.logoFileId)
|
||||
? this.fileUrlService.signFileByIdUrl({
|
||||
fileId: workspace.logoFileId,
|
||||
workspaceId: workspace.id,
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
})
|
||||
: undefined,
|
||||
allowImpersonation: workspace.allowImpersonation,
|
||||
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls({
|
||||
subdomain: workspace.subdomain,
|
||||
customDomain: workspace.customDomain,
|
||||
isCustomDomainEnabled: workspace.isCustomDomainEnabled,
|
||||
}),
|
||||
users: workspaceUsers
|
||||
.filter((wu) => isDefined(wu.user))
|
||||
.map((wu) => ({
|
||||
id: wu.user.id,
|
||||
email: wu.user.email,
|
||||
firstName: wu.user.firstName,
|
||||
lastName: wu.user.lastName,
|
||||
createdAt: wu.user.createdAt,
|
||||
})),
|
||||
featureFlags: allFeatureFlagKeys.map((key) => ({
|
||||
key,
|
||||
value: featureFlags.find((flag) => flag.key === key)?.value ?? false,
|
||||
})) as FeatureFlagEntity[],
|
||||
};
|
||||
|
||||
const firstUser = workspaceUsers.find((wu) => isDefined(wu.user))?.user;
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: firstUser?.id ?? '',
|
||||
email: firstUser?.email ?? '',
|
||||
firstName: firstUser?.firstName,
|
||||
lastName: firstUser?.lastName,
|
||||
createdAt: firstUser?.createdAt ?? new Date(),
|
||||
},
|
||||
workspaces: [workspaceInfo],
|
||||
};
|
||||
}
|
||||
|
||||
async getRecentUsers(
|
||||
searchTerm?: string,
|
||||
): Promise<AdminPanelRecentUserDTO[]> {
|
||||
let whereClause = 'u."deletedAt" IS NULL';
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (searchTerm && searchTerm.trim().length > 0) {
|
||||
const term = `%${searchTerm.trim()}%`;
|
||||
|
||||
whereClause += ` AND (u.email ILIKE $1 OR CONCAT(u."firstName", ' ', u."lastName") ILIKE $1 OR u.id::text ILIKE $1)`;
|
||||
params.push(term);
|
||||
}
|
||||
|
||||
const results = await this.userRepository.manager.query(
|
||||
`SELECT u.id, u.email, u."firstName", u."lastName", u."createdAt",
|
||||
w."displayName" AS "workspaceName", w.id AS "workspaceId"
|
||||
FROM core."user" u
|
||||
LEFT JOIN core."userWorkspace" uw ON uw."userId" = u.id AND uw."deletedAt" IS NULL
|
||||
LEFT JOIN core.workspace w ON w.id = uw."workspaceId" AND w."deletedAt" IS NULL
|
||||
WHERE ${whereClause}
|
||||
ORDER BY u."createdAt" DESC
|
||||
LIMIT 10`,
|
||||
params,
|
||||
);
|
||||
|
||||
return results.map(
|
||||
(row: {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
createdAt: Date;
|
||||
workspaceName: string | null;
|
||||
workspaceId: string | null;
|
||||
}) => ({
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
firstName: row.firstName || null,
|
||||
lastName: row.lastName || null,
|
||||
createdAt: row.createdAt,
|
||||
workspaceName: row.workspaceName ?? null,
|
||||
workspaceId: row.workspaceId ?? null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getTopWorkspaces(
|
||||
searchTerm?: string,
|
||||
): Promise<AdminPanelTopWorkspaceDTO[]> {
|
||||
let whereClause = 'w."deletedAt" IS NULL';
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (searchTerm && searchTerm.trim().length > 0) {
|
||||
const term = `%${searchTerm.trim()}%`;
|
||||
|
||||
whereClause += ` AND (w."displayName" ILIKE $1 OR w.subdomain ILIKE $1 OR w.id::text ILIKE $1)`;
|
||||
params.push(term);
|
||||
}
|
||||
|
||||
const results = await this.workspaceRepository.manager.query(
|
||||
`SELECT w.id, w."displayName" AS name, w.subdomain, COUNT(uw.id)::int AS "totalUsers"
|
||||
FROM core.workspace w
|
||||
LEFT JOIN core."userWorkspace" uw ON uw."workspaceId" = w.id AND uw."deletedAt" IS NULL
|
||||
WHERE ${whereClause}
|
||||
GROUP BY w.id
|
||||
ORDER BY "totalUsers" DESC
|
||||
LIMIT 10`,
|
||||
params,
|
||||
);
|
||||
|
||||
return results.map(
|
||||
(row: {
|
||||
id: string;
|
||||
name: string;
|
||||
subdomain: string;
|
||||
totalUsers: number;
|
||||
}) => ({
|
||||
id: row.id,
|
||||
name: row.name ?? '',
|
||||
subdomain: row.subdomain ?? '',
|
||||
totalUsers: row.totalUsers,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async assertWorkspaceAllowsImpersonation(
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
select: { id: true, allowImpersonation: true },
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new UserInputError('Workspace not found');
|
||||
}
|
||||
|
||||
if (!workspace.allowImpersonation) {
|
||||
throw new UserInputError('This workspace has not enabled support access');
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkspaceChatThreads(
|
||||
workspaceId: string,
|
||||
): Promise<AdminWorkspaceChatThreadDTO[]> {
|
||||
await this.assertWorkspaceAllowsImpersonation(workspaceId);
|
||||
|
||||
const threads = await this.agentChatThreadRepository.find({
|
||||
where: { workspaceId },
|
||||
order: { updatedAt: 'DESC' },
|
||||
take: 100,
|
||||
});
|
||||
|
||||
return threads.map((thread) => ({
|
||||
id: thread.id,
|
||||
title: thread.title,
|
||||
totalInputTokens: thread.totalInputTokens,
|
||||
totalOutputTokens: thread.totalOutputTokens,
|
||||
conversationSize: thread.conversationSize,
|
||||
createdAt: thread.createdAt,
|
||||
updatedAt: thread.updatedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
async getChatThreadMessages(
|
||||
workspaceId: string,
|
||||
threadId: string,
|
||||
): Promise<{
|
||||
thread: AdminWorkspaceChatThreadDTO;
|
||||
messages: AdminChatMessageDTO[];
|
||||
}> {
|
||||
await this.assertWorkspaceAllowsImpersonation(workspaceId);
|
||||
|
||||
const thread = await this.agentChatThreadRepository.findOne({
|
||||
where: { id: threadId, workspaceId },
|
||||
});
|
||||
|
||||
if (!thread) {
|
||||
throw new UserInputError('Thread not found in this workspace');
|
||||
}
|
||||
|
||||
const messages = await this.agentMessageRepository.find({
|
||||
where: { threadId },
|
||||
relations: { parts: true },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
|
||||
return {
|
||||
thread: {
|
||||
id: thread.id,
|
||||
title: thread.title,
|
||||
totalInputTokens: thread.totalInputTokens,
|
||||
totalOutputTokens: thread.totalOutputTokens,
|
||||
conversationSize: thread.conversationSize,
|
||||
createdAt: thread.createdAt,
|
||||
updatedAt: thread.updatedAt,
|
||||
},
|
||||
messages: messages.map((message) => ({
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
parts: (message.parts ?? [])
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
.map((part) => ({
|
||||
type: part.type,
|
||||
textContent: part.textContent,
|
||||
toolName: part.toolName,
|
||||
})),
|
||||
createdAt: message.createdAt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
getConfigVariablesGrouped(): ConfigVariablesDTO {
|
||||
const rawEnvVars = this.twentyConfigService.getAll();
|
||||
const groupedData = new Map<ConfigVariablesGroup, ConfigVariableDTO[]>();
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('AdminChatMessagePart')
|
||||
export class AdminChatMessagePartDTO {
|
||||
@Field(() => String)
|
||||
type: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
textContent: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
toolName: string | null;
|
||||
}
|
||||
|
||||
@ObjectType('AdminChatMessage')
|
||||
export class AdminChatMessageDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
role: string;
|
||||
|
||||
@Field(() => [AdminChatMessagePartDTO])
|
||||
parts: AdminChatMessagePartDTO[];
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { AdminChatMessageDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-chat-message.dto';
|
||||
import { AdminWorkspaceChatThreadDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-workspace-chat-thread.dto';
|
||||
|
||||
@ObjectType('AdminChatThreadMessages')
|
||||
export class AdminChatThreadMessagesDTO {
|
||||
@Field(() => AdminWorkspaceChatThreadDTO)
|
||||
thread: AdminWorkspaceChatThreadDTO;
|
||||
|
||||
@Field(() => [AdminChatMessageDTO])
|
||||
messages: AdminChatMessageDTO[];
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('AdminPanelRecentUser')
|
||||
export class AdminPanelRecentUserDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
email: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
firstName?: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
lastName?: string | null;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
workspaceName?: string | null;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
workspaceId?: string | null;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('AdminPanelTopWorkspace')
|
||||
export class AdminPanelTopWorkspaceDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => Int)
|
||||
totalUsers: number;
|
||||
|
||||
@Field(() => String)
|
||||
subdomain: string;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('AdminWorkspaceChatThread')
|
||||
export class AdminWorkspaceChatThreadDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
title: string | null;
|
||||
|
||||
@Field(() => Int)
|
||||
totalInputTokens: number;
|
||||
|
||||
@Field(() => Int)
|
||||
totalOutputTokens: number;
|
||||
|
||||
@Field(() => Int)
|
||||
conversationSize: number;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
|
||||
@Field(() => Date)
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { FeatureFlagDTO } from 'src/engine/core-modules/feature-flag/dtos/feature-flag.dto';
|
||||
import { WorkspaceUrlsDTO } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
|
||||
@@ -17,6 +19,9 @@ class UserInfoDTO {
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@ObjectType('WorkspaceInfo')
|
||||
@@ -36,6 +41,12 @@ class WorkspaceInfoDTO {
|
||||
@Field(() => Number)
|
||||
totalUsers: number;
|
||||
|
||||
@Field(() => WorkspaceActivationStatus)
|
||||
activationStatus: WorkspaceActivationStatus;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
|
||||
@Field(() => WorkspaceUrlsDTO)
|
||||
workspaceUrls: WorkspaceUrlsDTO;
|
||||
|
||||
|
||||
+50
-33
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
import type { UIDataTypes, UIMessagePart, UITools } from 'ai';
|
||||
|
||||
@@ -122,18 +123,19 @@ export class AgentChatService {
|
||||
let actualTurnId = turnId;
|
||||
|
||||
if (!actualTurnId) {
|
||||
const turn = this.turnRepository.create({
|
||||
const turnInsertResult = await this.turnRepository.insert({
|
||||
threadId,
|
||||
agentId: agentId ?? null,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const savedTurn = await this.turnRepository.save(turn);
|
||||
|
||||
actualTurnId = savedTurn.id;
|
||||
actualTurnId = turnInsertResult.identifiers[0].id as string;
|
||||
}
|
||||
|
||||
const message = this.messageRepository.create({
|
||||
// Use .insert() instead of .create() + .save() to bypass a TypeORM
|
||||
// issue where @ManyToOne/@JoinColumn on the same column as @Column can
|
||||
// cause the explicit workspaceId value to be dropped during .save().
|
||||
const messageValues = {
|
||||
...(id ? { id } : {}),
|
||||
threadId,
|
||||
turnId: actualTurnId,
|
||||
@@ -141,21 +143,33 @@ export class AgentChatService {
|
||||
agentId: agentId ?? null,
|
||||
processedAt: new Date(),
|
||||
workspaceId,
|
||||
});
|
||||
};
|
||||
|
||||
const savedMessage = await this.messageRepository.save(message);
|
||||
const insertResult = await this.messageRepository.insert(messageValues);
|
||||
|
||||
const savedMessageId = (id ?? insertResult.identifiers[0].id) as string;
|
||||
|
||||
if (uiMessage.parts && uiMessage.parts.length > 0) {
|
||||
const dbParts = mapUIMessagePartsToDBParts(
|
||||
uiMessage.parts,
|
||||
savedMessage.id,
|
||||
savedMessageId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.messagePartRepository.save(dbParts);
|
||||
await this.messagePartRepository.insert(
|
||||
dbParts as QueryDeepPartialEntity<AgentMessagePartEntity>[],
|
||||
);
|
||||
}
|
||||
|
||||
return savedMessage;
|
||||
return {
|
||||
id: savedMessageId,
|
||||
threadId,
|
||||
turnId: actualTurnId,
|
||||
role: uiMessage.role as AgentMessageRole,
|
||||
agentId: agentId ?? null,
|
||||
processedAt: new Date(),
|
||||
workspaceId,
|
||||
} as AgentMessageEntity;
|
||||
}
|
||||
|
||||
async getMessagesForThread(threadId: string, userWorkspaceId: string) {
|
||||
@@ -193,7 +207,7 @@ export class AgentChatService {
|
||||
fileIds?: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<AgentMessageEntity> {
|
||||
const message = this.messageRepository.create({
|
||||
const messageValues = {
|
||||
...(id ? { id } : {}),
|
||||
threadId,
|
||||
turnId: null,
|
||||
@@ -201,9 +215,11 @@ export class AgentChatService {
|
||||
agentId: null,
|
||||
status: AgentMessageStatus.QUEUED,
|
||||
workspaceId,
|
||||
});
|
||||
};
|
||||
|
||||
const savedMessage = await this.messageRepository.save(message);
|
||||
const insertResult = await this.messageRepository.insert(messageValues);
|
||||
|
||||
const savedMessageId = (id ?? insertResult.identifiers[0].id) as string;
|
||||
|
||||
const files =
|
||||
fileIds && fileIds.length > 0
|
||||
@@ -213,28 +229,29 @@ export class AgentChatService {
|
||||
: [];
|
||||
|
||||
const parts = [
|
||||
this.messagePartRepository.create({
|
||||
messageId: savedMessage.id,
|
||||
{
|
||||
messageId: savedMessageId,
|
||||
orderIndex: 0,
|
||||
type: 'text',
|
||||
textContent: text,
|
||||
workspaceId,
|
||||
}),
|
||||
...files.map((file, index) =>
|
||||
this.messagePartRepository.create({
|
||||
messageId: savedMessage.id,
|
||||
orderIndex: index + 1,
|
||||
type: 'file',
|
||||
fileId: file.id,
|
||||
fileFilename: file.path.split('/').pop() ?? null,
|
||||
workspaceId,
|
||||
}),
|
||||
),
|
||||
},
|
||||
...files.map((file, index) => ({
|
||||
messageId: savedMessageId,
|
||||
orderIndex: index + 1,
|
||||
type: 'file',
|
||||
fileId: file.id,
|
||||
fileFilename: file.path.split('/').pop() ?? null,
|
||||
workspaceId,
|
||||
})),
|
||||
];
|
||||
|
||||
await this.messagePartRepository.save(parts);
|
||||
await this.messagePartRepository.insert(parts);
|
||||
|
||||
return savedMessage;
|
||||
return {
|
||||
id: savedMessageId,
|
||||
...messageValues,
|
||||
} as AgentMessageEntity;
|
||||
}
|
||||
|
||||
async getQueuedMessages(threadId: string): Promise<AgentMessageEntity[]> {
|
||||
@@ -270,30 +287,30 @@ export class AgentChatService {
|
||||
threadId: string,
|
||||
workspaceId: string,
|
||||
): Promise<string | null> {
|
||||
const turn = this.turnRepository.create({
|
||||
const turnInsertResult = await this.turnRepository.insert({
|
||||
threadId,
|
||||
agentId: null,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const savedTurn = await this.turnRepository.save(turn);
|
||||
const savedTurnId = turnInsertResult.identifiers[0].id as string;
|
||||
|
||||
const result = await this.messageRepository.update(
|
||||
{ id: messageId, threadId, status: AgentMessageStatus.QUEUED },
|
||||
{
|
||||
status: AgentMessageStatus.SENT,
|
||||
processedAt: new Date(),
|
||||
turnId: savedTurn.id,
|
||||
turnId: savedTurnId,
|
||||
},
|
||||
);
|
||||
|
||||
if ((result.affected ?? 0) === 0) {
|
||||
await this.turnRepository.delete(savedTurn.id);
|
||||
await this.turnRepository.delete(savedTurnId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return savedTurn.id;
|
||||
return savedTurnId;
|
||||
}
|
||||
|
||||
async generateTitleIfNeeded({
|
||||
|
||||
@@ -69,6 +69,9 @@ export enum SettingsPath {
|
||||
AdminPanelNewAiProvider = 'admin-panel/ai/new-provider',
|
||||
AdminPanelAiProviderDetail = 'admin-panel/ai/providers/:providerName',
|
||||
AdminPanelNewAiModel = 'admin-panel/ai/providers/:providerName/new-model',
|
||||
AdminPanelUserDetail = 'admin-panel/users/:userId',
|
||||
AdminPanelWorkspaceDetail = 'admin-panel/workspaces/:workspaceId',
|
||||
AdminPanelWorkspaceChatThread = 'admin-panel/workspaces/:workspaceId/threads/:threadId',
|
||||
|
||||
Roles = 'roles',
|
||||
RoleCreate = 'roles/create',
|
||||
|
||||
Reference in New Issue
Block a user