wip: AI user

This commit is contained in:
Robin Shen 2025-11-24 16:46:29 +08:00
parent 9fdbf390b3
commit b9ee6f987b
134 changed files with 3601 additions and 486 deletions

View File

@ -627,6 +627,11 @@
<artifactId>fastexcel</artifactId>
<version>0.15.7</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>

View File

@ -383,6 +383,10 @@
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
</dependency>
</dependencies>
<properties>
<kotlin.version>1.9.23</kotlin.version>

View File

@ -0,0 +1,327 @@
package dev.langchain4j.internal;
import java.util.Random;
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dev.langchain4j.Internal;
import dev.langchain4j.exception.LangChain4jException;
import dev.langchain4j.exception.NonRetriableException;
import io.onedev.server.exception.ExceptionUtils;
/**
* Utility class for retrying actions.
*/
@Internal
public final class RetryUtils {
private static final Random RANDOM = new Random();
private RetryUtils() {}
private static final Logger log = LoggerFactory.getLogger(RetryUtils.class);
/**
* This method returns a RetryPolicy.Builder.
*
* @return A RetryPolicy.Builder.
*/
public static RetryPolicy.Builder retryPolicyBuilder() {
return new RetryPolicy.Builder();
}
/**
* This class encapsulates a retry policy.
*/
public static final class RetryPolicy {
/**
* This class encapsulates a retry policy builder.
*/
public static final class Builder {
private int maxRetries = 2;
private int delayMillis = 1000;
private double jitterScale = 0.2;
private double backoffExp = 1.5;
/**
* Construct a RetryPolicy.Builder.
*/
public Builder() {}
/**
* Sets the default maximum number of retries.
*
* @param maxRetries The maximum number of retries.
* The action can be executed up to {@code maxRetries + 1} times.
* @return {@code this}
*/
public Builder maxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}
/**
* Sets the base delay in milliseconds.
*
* <p>The delay is calculated as follows:
* <ol>
* <li>Calculate the raw delay in milliseconds as
* {@code delayMillis * Math.pow(backoffExp, retry)}.</li>
* <li>Calculate the jitter delay in milliseconds as
* {@code rawDelayMs + rand.nextInt((int) (rawDelayMs * jitterScale))}.</li>
* <li>Sleep for the jitter delay in milliseconds.</li>
* </ol>
*
* @param delayMillis The delay in milliseconds.
* @return {@code this}
*/
public Builder delayMillis(int delayMillis) {
this.delayMillis = delayMillis;
return this;
}
/**
* Sets the jitter scale.
*
* <p>The jitter delay in milliseconds is calculated as
* {@code rawDelayMs + rand.nextInt((int) (rawDelayMs * jitterScale))}.
*
* @param jitterScale The jitter scale.
* @return {@code this}
*/
public Builder jitterScale(double jitterScale) {
this.jitterScale = jitterScale;
return this;
}
/**
* Sets the backoff exponent.
*
* @param backoffExp The backoff exponent.
* @return {@code this}
*/
public Builder backoffExp(double backoffExp) {
this.backoffExp = backoffExp;
return this;
}
/**
* Builds a RetryPolicy.
*
* @return A RetryPolicy.
*/
public RetryPolicy build() {
return new RetryPolicy(maxRetries, delayMillis, jitterScale, backoffExp);
}
}
private final int maxRetries;
private final int delayMillis;
private final double jitterScale;
private final double backoffExp;
/**
* Construct a RetryPolicy.
*
* @param maxRetries The maximum number of retries.
* The action can be executed up to {@code maxRetries + 1} times.
* @param delayMillis The delay in milliseconds.
* @param jitterScale The jitter scale.
* @param backoffExp The backoff exponent.
*/
public RetryPolicy(int maxRetries, int delayMillis, double jitterScale, double backoffExp) {
this.maxRetries = maxRetries;
this.delayMillis = delayMillis;
this.jitterScale = jitterScale;
this.backoffExp = backoffExp;
}
/**
* This method returns the raw delay in milliseconds after a given retry.
*
* @param retry The retry number.
* @return The raw delay in milliseconds.
*/
public double rawDelayMs(int retry) {
return delayMillis * Math.pow(backoffExp, retry);
}
/**
* This method returns the jitter delay in milliseconds after a given retry.
*
* @param retry The retry number.
* @return The jitter delay in milliseconds.
*/
public int jitterDelayMillis(int retry) {
double delay = rawDelayMs(retry);
double jitter = delay * jitterScale;
return (int) (delay + RANDOM.nextInt((int) jitter));
}
/**
* This method sleeps after a given retry.
*
* @param retry The retry number.
*/
@JacocoIgnoreCoverageGenerated
public void sleep(int retry) {
try {
Thread.sleep(jitterDelayMillis(retry));
} catch (InterruptedException ignored) {
// pass
}
}
/**
* This method attempts to execute a given action up to 3 times with an exponential backoff.
* If the action fails on all attempts, it throws a RuntimeException.
*
* @param action The action to be executed.
* @param <T> The type of the result of the action.
* @return The result of the action if it is successful.
* @throws RuntimeException if the action fails on all attempts.
*/
public <T> T withRetry(Callable<T> action) {
return withRetry(action, maxRetries);
}
/**
* This method attempts to execute a given action up to a specified number of times with an exponential backoff.
* If the action fails on all attempts, it throws a RuntimeException.
*
* @param action The action to be executed.
* @param maxRetries The maximum number of retries.
* The action can be executed up to {@code maxRetries + 1} times.
* @param <T> The type of the result of the action.
* @return The result of the action if it is successful.
* @throws RuntimeException if the action fails on all attempts.
*/
public <T> T withRetry(Callable<T> action, int maxRetries) {
int retry = 0;
while (true) {
try {
return action.call();
} catch (NonRetriableException e) {
throw e;
} catch (Exception e) {
if (retry >= maxRetries || ExceptionUtils.find(e, InterruptedException.class) != null) {
throw e instanceof RuntimeException re ? re : new LangChain4jException(e);
}
log.warn(
"A retriable exception occurred. Remaining retries: %s of %s"
.formatted(maxRetries - retry, maxRetries),
e);
sleep(retry);
}
retry++;
}
}
}
/**
* Default retry policy used by {@link #withRetry(Callable)}.
*/
public static final RetryPolicy DEFAULT_RETRY_POLICY = retryPolicyBuilder()
.maxRetries(2)
.delayMillis(500)
.jitterScale(0.2)
.backoffExp(1.5)
.build();
/**
* This method attempts to execute a given action up to 3 times with an exponential backoff.
* If the action fails on all attempts, it throws a RuntimeException.
*
* @param action The action to be executed.
* @param <T> The type of the result of the action.
* @return The result of the action if it is successful.
* @throws RuntimeException if the action fails on all attempts.
*/
public static <T> T withRetry(Callable<T> action) {
return DEFAULT_RETRY_POLICY.withRetry(action);
}
/**
* This method attempts to execute a given action up to a specified number of times with an exponential backoff.
* If the action fails on all attempts, it throws a RuntimeException.
*
* @param action The action to be executed.
* @param maxRetries The maximum number of retries.
* The action can be executed up to {@code maxRetries + 1} times.
* @param <T> The type of the result of the action.
* @return The result of the action if it is successful.
* @throws RuntimeException if the action fails on all attempts.
*/
public static <T> T withRetry(Callable<T> action, int maxRetries) {
return DEFAULT_RETRY_POLICY.withRetry(action, maxRetries);
}
/**
* This method attempts to execute a given action up to a specified number of times with an exponential backoff.
* If the action fails on all attempts, it throws a RuntimeException.
*
* @param action The action to be executed.
* @param maxRetries The maximum number of retries.
* The action can be executed up to {@code maxRetries + 1} times.
* @throws RuntimeException if the action fails on all attempts.
*/
public static void withRetry(Runnable action, int maxRetries) {
DEFAULT_RETRY_POLICY.withRetry(
() -> {
action.run();
return null;
},
maxRetries);
}
/**
* This method attempts to execute a given action up to 3 times with an exponential backoff.
* If the action fails, the Exception causing the failure will be mapped with the default {@link ExceptionMapper}.
*
* @param action The action to be executed.
* @param <T> The type of the result of the action.
* @return The result of the action if it is successful.
* @throws RuntimeException if the action fails on all attempts.
*/
public static <T> T withRetryMappingExceptions(Callable<T> action) {
return withRetry(() -> ExceptionMapper.DEFAULT.withExceptionMapper(action));
}
/**
* This method attempts to execute a given action up to a specified number of times with an exponential backoff.
* If the action fails, the Exception causing the failure will be mapped with the default {@link ExceptionMapper}.
*
* @param action The action to be executed.
* @param maxRetries The maximum number of retries.
* The action can be executed up to {@code maxRetries + 1} times.
* @param <T> The type of the result of the action.
* @return The result of the action if it is successful.
* @throws RuntimeException if the action fails on all attempts.
*/
public static <T> T withRetryMappingExceptions(Callable<T> action, int maxRetries) {
return withRetryMappingExceptions(action, maxRetries, ExceptionMapper.DEFAULT);
}
/**
* This method attempts to execute a given action up to a specified number of times with an exponential backoff.
* If the action fails, the Exception causing the failure will be mapped with the provided {@link ExceptionMapper}.
*
* @param action The action to be executed.
* @param maxRetries The maximum number of retries.
* The action can be executed up to {@code maxRetries + 1} times.
* @param exceptionMapper The ExceptionMapper used to translate the exception that caused the failure of the action invocation.
* @param <T> The type of the result of the action.
* @return The result of the action if it is successful.
* @throws RuntimeException if the action fails on all attempts.
*/
public static <T> T withRetryMappingExceptions(
Callable<T> action, int maxRetries, ExceptionMapper exceptionMapper) {
return withRetry(() -> exceptionMapper.withExceptionMapper(action), maxRetries);
}
}

View File

@ -197,6 +197,7 @@ import io.onedev.server.service.BuildMetricService;
import io.onedev.server.service.BuildParamService;
import io.onedev.server.service.BuildQueryPersonalizationService;
import io.onedev.server.service.BuildService;
import io.onedev.server.service.ChatService;
import io.onedev.server.service.CodeCommentMentionService;
import io.onedev.server.service.CodeCommentQueryPersonalizationService;
import io.onedev.server.service.CodeCommentReplyService;
@ -212,6 +213,7 @@ import io.onedev.server.service.EmailAddressService;
import io.onedev.server.service.GitLfsLockService;
import io.onedev.server.service.GpgKeyService;
import io.onedev.server.service.GroupAuthorizationService;
import io.onedev.server.service.GroupEntitlementService;
import io.onedev.server.service.GroupService;
import io.onedev.server.service.IssueAuthorizationService;
import io.onedev.server.service.IssueChangeService;
@ -243,6 +245,7 @@ import io.onedev.server.service.PackLabelService;
import io.onedev.server.service.PackQueryPersonalizationService;
import io.onedev.server.service.PackService;
import io.onedev.server.service.PendingSuggestionApplyService;
import io.onedev.server.service.ProjectEntitlementService;
import io.onedev.server.service.ProjectLabelService;
import io.onedev.server.service.ProjectLastEventDateService;
import io.onedev.server.service.ProjectService;
@ -269,6 +272,7 @@ import io.onedev.server.service.SsoAccountService;
import io.onedev.server.service.SsoProviderService;
import io.onedev.server.service.StopwatchService;
import io.onedev.server.service.UserAuthorizationService;
import io.onedev.server.service.UserEntitlementService;
import io.onedev.server.service.UserInvitationService;
import io.onedev.server.service.UserService;
import io.onedev.server.service.impl.DefaultAccessTokenAuthorizationService;
@ -285,6 +289,7 @@ import io.onedev.server.service.impl.DefaultBuildMetricService;
import io.onedev.server.service.impl.DefaultBuildParamService;
import io.onedev.server.service.impl.DefaultBuildQueryPersonalizationService;
import io.onedev.server.service.impl.DefaultBuildService;
import io.onedev.server.service.impl.DefaultChatService;
import io.onedev.server.service.impl.DefaultCodeCommentMentionService;
import io.onedev.server.service.impl.DefaultCodeCommentQueryPersonalizationService;
import io.onedev.server.service.impl.DefaultCodeCommentReplyService;
@ -300,6 +305,7 @@ import io.onedev.server.service.impl.DefaultEmailAddressService;
import io.onedev.server.service.impl.DefaultGitLfsLockService;
import io.onedev.server.service.impl.DefaultGpgKeyService;
import io.onedev.server.service.impl.DefaultGroupAuthorizationService;
import io.onedev.server.service.impl.DefaultGroupEntitlementService;
import io.onedev.server.service.impl.DefaultGroupService;
import io.onedev.server.service.impl.DefaultIssueAuthorizationService;
import io.onedev.server.service.impl.DefaultIssueChangeService;
@ -331,6 +337,7 @@ import io.onedev.server.service.impl.DefaultPackLabelService;
import io.onedev.server.service.impl.DefaultPackQueryPersonalizationService;
import io.onedev.server.service.impl.DefaultPackService;
import io.onedev.server.service.impl.DefaultPendingSuggestionApplyService;
import io.onedev.server.service.impl.DefaultProjectEntitlementService;
import io.onedev.server.service.impl.DefaultProjectLabelService;
import io.onedev.server.service.impl.DefaultProjectLastEventDateService;
import io.onedev.server.service.impl.DefaultProjectService;
@ -357,6 +364,7 @@ import io.onedev.server.service.impl.DefaultSsoAccountService;
import io.onedev.server.service.impl.DefaultSsoProviderService;
import io.onedev.server.service.impl.DefaultStopwatchService;
import io.onedev.server.service.impl.DefaultUserAuthorizationService;
import io.onedev.server.service.impl.DefaultUserEntitlementService;
import io.onedev.server.service.impl.DefaultUserInvitationService;
import io.onedev.server.service.impl.DefaultUserService;
import io.onedev.server.ssh.CommandCreator;
@ -393,6 +401,7 @@ import io.onedev.server.web.DefaultUrlService;
import io.onedev.server.web.DefaultWicketFilter;
import io.onedev.server.web.DefaultWicketServlet;
import io.onedev.server.web.ResourcePackScopeContribution;
import io.onedev.server.web.SessionListener;
import io.onedev.server.web.UrlService;
import io.onedev.server.web.WebApplication;
import io.onedev.server.web.avatar.AvatarService;
@ -600,7 +609,11 @@ public class CoreModule extends AbstractPluginModule {
bind(PullRequestDescriptionRevisionService.class).to(DefaultPullRequestDescriptionRevisionService.class);
bind(SsoProviderService.class).to(DefaultSsoProviderService.class);
bind(SsoAccountService.class).to(DefaultSsoAccountService.class);
bind(ChatService.class).to(DefaultChatService.class);
bind(BaseAuthorizationService.class).to(DefaultBaseAuthorizationService.class);
bind(GroupEntitlementService.class).to(DefaultGroupEntitlementService.class);
bind(UserEntitlementService.class).to(DefaultUserEntitlementService.class);
bind(ProjectEntitlementService.class).to(DefaultProjectEntitlementService.class);
bind(WebHookManager.class);
@ -719,6 +732,9 @@ public class CoreModule extends AbstractPluginModule {
contributeFromPackage(ExceptionHandler.class, ConstraintViolationExceptionHandler.class);
contributeFromPackage(ExceptionHandler.class, PageExpiredExceptionHandler.class);
contributeFromPackage(ExceptionHandler.class, WebApplicationExceptionHandler.class);
contribute(SessionListener.class, DefaultChatService.class);
contribute(SessionListener.class, DefaultWebSocketService.class);
bind(UrlService.class).to(DefaultUrlService.class);
bind(CodeCommentEventBroadcaster.class);

View File

@ -2,7 +2,7 @@ package io.onedev.server.data;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static io.onedev.server.model.User.PROP_NOTIFY_OWN_EVENTS;
import static io.onedev.server.model.User.PROP_SERVICE_ACCOUNT;
import static io.onedev.server.model.User.PROP_TYPE;
import static io.onedev.server.model.support.administration.SystemSetting.PROP_CURL_LOCATION;
import static io.onedev.server.model.support.administration.SystemSetting.PROP_DISABLE_AUTO_UPDATE_CHECK;
import static io.onedev.server.model.support.administration.SystemSetting.PROP_GIT_LOCATION;
@ -92,7 +92,7 @@ import io.onedev.server.model.Role;
import io.onedev.server.model.Setting;
import io.onedev.server.model.Setting.Key;
import io.onedev.server.model.User;
import io.onedev.server.model.support.administration.AISetting;
import io.onedev.server.model.support.administration.AiSetting;
import io.onedev.server.model.support.administration.AgentSetting;
import io.onedev.server.model.support.administration.AlertSetting;
import io.onedev.server.model.support.administration.AuditSetting;
@ -674,7 +674,7 @@ public class DefaultDataService implements DataService, Serializable {
if (validator.validate(bean).isEmpty()) {
createRoot(bean);
} else {
manualConfigs.add(new ManualConfig("Create Administrator Account", null, bean, Sets.newHashSet(PROP_SERVICE_ACCOUNT, PROP_NOTIFY_OWN_EVENTS)) {
manualConfigs.add(new ManualConfig("Create Administrator Account", null, bean, Sets.newHashSet(PROP_TYPE, PROP_NOTIFY_OWN_EVENTS)) {
@Override
public void complete() {
@ -884,7 +884,7 @@ public class DefaultDataService implements DataService, Serializable {
setting = settingService.findSetting(Key.AI);
if (setting == null)
settingService.saveAISetting(new AISetting());
settingService.saveAiSetting(new AiSetting());
if (roleService.get(Role.OWNER_ID) == null) {
Role owner = new Role();

View File

@ -8382,4 +8382,19 @@ public class DataMigrator {
private void migrate214(File dataDir, Stack<Integer> versions) {
}
private void migrate215(File dataDir, Stack<Integer> versions) {
for (File file : dataDir.listFiles()) {
if (file.getName().startsWith("Users.xml")) {
VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
for (Element element : dom.getRootElement().elements()) {
Element serviceAccountElement = element.element("serviceAccount");
boolean isServiceAccount = Boolean.parseBoolean(serviceAccountElement.getTextTrim());
serviceAccountElement.detach();
element.addElement("type").setText(isServiceAccount ? "SERVICE" : "ORDINARY");
}
dom.writeToFile(file, false);
}
}
}
}

View File

@ -26,7 +26,7 @@ public class IssueChanged extends IssueEvent implements CommitAware {
private final String comment;
public IssueChanged(IssueChange change, @Nullable String comment) {
this(change, comment, !change.getUser().isServiceAccount());
this(change, comment, change.getUser().getType() != User.Type.SERVICE);
}
public IssueChanged(IssueChange change, @Nullable String comment, boolean sendNotifications) {

View File

@ -22,7 +22,7 @@ public abstract class IssueEvent extends ProjectEvent {
private final boolean sendNotifications;
public IssueEvent(User user, Date date, Issue issue) {
this(user, date, issue, !user.isServiceAccount());
this(user, date, issue, user.getType() != User.Type.SERVICE);
}
public IssueEvent(User user, Date date, Issue issue, boolean sendNotifications) {

View File

@ -0,0 +1,116 @@
package io.onedev.server.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import org.apache.commons.lang3.StringUtils;
@Entity
@Table(indexes={@Index(columnList="o_user_id"), @Index(columnList="o_ai_id")})
public class Chat extends AbstractEntity {
private static final long serialVersionUID = 1L;
public static final int MAX_TITLE_LEN = 255;
public static final String PROP_USER = "user";
public static final String PROP_AI = "ai";
public static final String PROP_DATE = "date";
public static final String PROP_TITLE = "title";
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private User user;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private User ai;
@Column(nullable=false)
private Date date;
@Column(nullable=false, length=MAX_TITLE_LEN)
private String title = "New chat";
@OneToMany(mappedBy="chat", cascade=CascadeType.REMOVE)
private Collection<ChatMessage> messages = new ArrayList<>();
private transient List<ChatMessage> sortedMessages;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public User getAi() {
return ai;
}
public void setAi(User ai) {
this.ai = ai;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = StringUtils.abbreviate(title, MAX_TITLE_LEN);
}
public Collection<ChatMessage> getMessages() {
return messages;
}
public void setMessages(Collection<ChatMessage> messages) {
this.messages = messages;
}
public List<ChatMessage> getSortedMessages() {
if (sortedMessages == null) {
sortedMessages = new ArrayList<>(messages);
sortedMessages.sort(Comparator.comparing(ChatMessage::getId));
}
return sortedMessages;
}
public static String getChangeObservable(Long chatId) {
return "chat:" + chatId;
}
public static String getPartialResponseObservable(Long chatId) {
return "chat:" + chatId + ":partialResponse";
}
public static String getNewMessagesObservable(Long chatId) {
return "chat:" + chatId + ":newMessages";
}
}

View File

@ -0,0 +1,80 @@
package io.onedev.server.model;
import java.util.LinkedHashMap;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import io.onedev.commons.utils.StringUtils;
@Entity
@Table(indexes={@Index(columnList="o_chat_id")})
public class ChatMessage extends AbstractEntity {
private static final long serialVersionUID = 1L;
private static final int MAX_CONTENT_LEN = 100000;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private Chat chat;
private boolean error;
private boolean request;
@Lob
@Column(nullable=false, length=MAX_CONTENT_LEN)
private String content;
@Lob
@Column(nullable=false, length=65535)
private LinkedHashMap<String, String> attachments = new LinkedHashMap<>();
public Chat getChat() {
return chat;
}
public void setChat(Chat chat) {
this.chat = chat;
}
public String getContent() {
return content;
}
public boolean isError() {
return error;
}
public void setError(boolean error) {
this.error = error;
}
public boolean isRequest() {
return request;
}
public void setRequest(boolean request) {
this.request = request;
}
public void setContent(String content) {
this.content = StringUtils.abbreviate(content, MAX_CONTENT_LEN);
}
public LinkedHashMap<String, String> getAttachments() {
return attachments;
}
public void setAttachments(LinkedHashMap<String, String> attachments) {
this.attachments = attachments;
}
}

View File

@ -59,6 +59,10 @@ public class Group extends AbstractEntity implements BasePermission {
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<Membership> memberships = new ArrayList<>();
@OneToMany(mappedBy="group", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<GroupEntitlement> entitlements = new ArrayList<>();
private transient Collection<User> members;
@Editable(order=100)
@ -133,6 +137,14 @@ public class Group extends AbstractEntity implements BasePermission {
this.memberships = memberships;
}
public Collection<GroupEntitlement> getEntitlements() {
return entitlements;
}
public void setEntitlements(Collection<GroupEntitlement> groupEntitlements) {
this.entitlements = groupEntitlements;
}
public Collection<User> getMembers() {
if (members == null) {
members = new HashSet<>();

View File

@ -0,0 +1,52 @@
package io.onedev.server.model;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Table(
indexes={@Index(columnList="o_ai_id"), @Index(columnList="o_group_id")},
uniqueConstraints={@UniqueConstraint(columnNames={"o_ai_id", "o_group_id"})
})
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class GroupEntitlement extends AbstractEntity {
private static final long serialVersionUID = 1L;
public static final String PROP_AI = "ai";
public static final String PROP_GROUP = "group";
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private User ai;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private Group group;
public User getAi() {
return ai;
}
public void setAi(User ai) {
this.ai = ai;
}
public Group getGroup() {
return group;
}
public void setGroup(Group group) {
this.group = group;
}
}

View File

@ -367,6 +367,10 @@ public class Project extends AbstractEntity implements LabelSupport<ProjectLabel
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<Iteration> iterations = new ArrayList<>();
@OneToMany(mappedBy="project", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<ProjectEntitlement> entitlements = new ArrayList<>();
@OneToMany(mappedBy="project", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<Audit> audits = new ArrayList<>();

View File

@ -0,0 +1,52 @@
package io.onedev.server.model;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Table(
indexes={@Index(columnList="o_ai_id"), @Index(columnList="o_project_id")},
uniqueConstraints={@UniqueConstraint(columnNames={"o_ai_id", "o_project_id"})
})
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class ProjectEntitlement extends AbstractEntity {
private static final long serialVersionUID = 1L;
public static String PROP_AI = "ai";
public static String PROP_PROJECT = "project";
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private Project project;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private User ai;
public Project getProject() {
return project;
}
public void setProject(Project project) {
this.project = project;
}
public User getAi() {
return ai;
}
public void setAi(User ai) {
this.ai = ai;
}
}

View File

@ -2,12 +2,14 @@ package io.onedev.server.model;
import static io.onedev.server.model.User.PROP_FULL_NAME;
import static io.onedev.server.model.User.PROP_NAME;
import static io.onedev.server.model.User.Type.AI;
import static io.onedev.server.model.User.Type.SERVICE;
import static io.onedev.server.security.SecurityUtils.asPrincipals;
import static io.onedev.server.security.SecurityUtils.asUserPrincipal;
import static io.onedev.server.web.translation.Translation._T;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@ -15,7 +17,6 @@ import java.util.Optional;
import java.util.Stack;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -31,6 +32,7 @@ import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.lib.PersonIdent;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.jspecify.annotations.Nullable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.base.MoreObjects;
@ -41,11 +43,8 @@ import io.onedev.server.OneDev;
import io.onedev.server.annotation.DependsOn;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.Password;
import io.onedev.server.annotation.SubscriptionRequired;
import io.onedev.server.annotation.UserName;
import io.onedev.server.service.EmailAddressService;
import io.onedev.server.service.SettingService;
import io.onedev.server.service.UserService;
import io.onedev.server.model.support.AiSetting;
import io.onedev.server.model.support.NamedProjectQuery;
import io.onedev.server.model.support.QueryPersonalization;
import io.onedev.server.model.support.TwoFactorAuthentication;
@ -54,10 +53,12 @@ import io.onedev.server.model.support.issue.NamedIssueQuery;
import io.onedev.server.model.support.pack.NamedPackQuery;
import io.onedev.server.model.support.pullrequest.NamedPullRequestQuery;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.service.EmailAddressService;
import io.onedev.server.service.SettingService;
import io.onedev.server.service.UserService;
import io.onedev.server.util.facade.UserFacade;
import io.onedev.server.util.watch.QuerySubscriptionSupport;
import io.onedev.server.util.watch.QueryWatchSupport;
import io.onedev.server.web.util.WicketUtils;
@Entity
@Table(indexes={@Index(columnList=PROP_NAME), @Index(columnList=PROP_FULL_NAME)})
@ -66,6 +67,8 @@ import io.onedev.server.web.util.WicketUtils;
public class User extends AbstractEntity implements AuthenticationInfo {
private static final long serialVersionUID = 1L;
public enum Type {ORDINARY, SERVICE, AI};
public static final Long UNKNOWN_ID = -2L;
@ -76,6 +79,8 @@ public class User extends AbstractEntity implements AuthenticationInfo {
public static final String SYSTEM_NAME = "OneDev";
public static final String SYSTEM_EMAIL_ADDRESS = "system@onedev";
public static final String AI_EMAIL_ADDRESS = "ai@onedev";
public static final String UNKNOWN_NAME = "unknown";
@ -83,7 +88,7 @@ public class User extends AbstractEntity implements AuthenticationInfo {
public static final String PROP_FULL_NAME = "fullName";
public static final String PROP_SERVICE_ACCOUNT = "serviceAccount";
public static final String PROP_TYPE = "type";
public static final String PROP_DISABLED = "disabled";
@ -93,7 +98,8 @@ public class User extends AbstractEntity implements AuthenticationInfo {
private static ThreadLocal<Stack<User>> stack = ThreadLocal.withInitial(() -> new Stack<>());
private boolean serviceAccount;
@Column(nullable=false)
private Type type = Type.ORDINARY;
private boolean disabled;
@ -109,6 +115,10 @@ public class User extends AbstractEntity implements AuthenticationInfo {
private String fullName;
private boolean notifyOwnEvents;
@Lob
@Column(length=65535)
private AiSetting aiSetting = new AiSetting();
@JsonIgnore
@Lob
@ -229,6 +239,30 @@ public class User extends AbstractEntity implements AuthenticationInfo {
@OneToMany(mappedBy=ReviewedDiff.PROP_USER, cascade=CascadeType.REMOVE)
private Collection<ReviewedDiff> reviewedDiffs = new ArrayList<>();
@OneToMany(mappedBy="ai", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<ProjectEntitlement> projectEntitlements = new ArrayList<>();
@OneToMany(mappedBy="ai", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<GroupEntitlement> groupEntitlements = new ArrayList<>();
@OneToMany(mappedBy="ai", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<UserEntitlement> userEntitlements = new ArrayList<>();
@OneToMany(mappedBy="user", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<UserEntitlement> aiEntitlements = new ArrayList<>();
@OneToMany(mappedBy="ai", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<Chat> aiChats = new ArrayList<>();
@OneToMany(mappedBy="user", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<Chat> userChats = new ArrayList<>();
@JsonIgnore
@Lob
@Column(nullable=false, length=65535)
@ -283,6 +317,8 @@ public class User extends AbstractEntity implements AuthenticationInfo {
private transient Optional<EmailAddress> gitEmailAddress;
private transient Optional<EmailAddress> publicEmailAddress;
private transient List<User> entitledAis;
public QueryPersonalization<NamedProjectQuery> getProjectQueryPersonalization() {
return new QueryPersonalization<NamedProjectQuery>() {
@ -531,14 +567,16 @@ public class User extends AbstractEntity implements AuthenticationInfo {
return SecurityUtils.asSubject(getPrincipals());
}
@Editable(order=50, name="Service Account", descriptionProvider = "getServiceAccountDescription")
@SubscriptionRequired
public boolean isServiceAccount() {
return serviceAccount;
@Editable(order=50, description = "" +
"Ordinary: Normal account<br>" +
"Service: Service account does not have password and email addresses, and will not generate notifications for its activities<br>" +
"AI: AI account (working in progress)")
public Type getType() {
return type;
}
public void setServiceAccount(boolean serviceAccount) {
this.serviceAccount = serviceAccount;
public void setType(Type type) {
this.type = type;
}
public boolean isDisabled() {
@ -549,20 +587,6 @@ public class User extends AbstractEntity implements AuthenticationInfo {
this.disabled = disabled;
}
@SuppressWarnings("unused")
private static String getServiceAccountDescription() {
if (!WicketUtils.isSubscriptionActive()) {
return _T(""
+ "Whether or not to create as a service account for task automation purpose. Service account does not have password and email addresses, and will not generate "
+ "notifications for its activities. <b class='text-warning'>NOTE:</b> Service account is an enterprise feature. "
+ "<a href='https://onedev.io/pricing' target='_blank'>Try free</a> for 30 days");
} else {
return _T(""
+ "Whether or not to create as a service account for task automation purpose. Service account does not have password and email addresses, and will not generate "
+ "notifications for its activities");
}
}
@Editable(name="Login Name", order=100)
@UserName
@NotEmpty
@ -580,7 +604,7 @@ public class User extends AbstractEntity implements AuthenticationInfo {
* time
*/
@Editable(order=150)
@DependsOn(property="serviceAccount", value="false")
@DependsOn(property="type", value="ORDINARY")
@Password(checkPolicy=true, autoComplete="new-password")
@NotEmpty
@Nullable
@ -614,9 +638,9 @@ public class User extends AbstractEntity implements AuthenticationInfo {
public void setFullName(String fullName) {
this.fullName = fullName;
}
@Editable(order=400, name="Notify Own Events", description = "Whether or not to send notifications for events generated by yourself")
@DependsOn(property="serviceAccount", value="false")
@DependsOn(property="type", value="ORDINARY")
public boolean isNotifyOwnEvents() {
return notifyOwnEvents;
}
@ -625,6 +649,14 @@ public class User extends AbstractEntity implements AuthenticationInfo {
this.notifyOwnEvents = sendOwnEvents;
}
public AiSetting getAiSetting() {
return aiSetting;
}
public void setAiSetting(AiSetting aiSetting) {
this.aiSetting = aiSetting;
}
public Collection<AccessToken> getAccessTokens() {
return accessTokens;
}
@ -666,7 +698,13 @@ public class User extends AbstractEntity implements AuthenticationInfo {
}
public PersonIdent asPerson() {
if (isSystem()) {
if (getType() == SERVICE) {
throw new ExplicitException("Service account does not have git identity");
} else if (getType() == AI) {
return new PersonIdent(getName(), User.AI_EMAIL_ADDRESS);
} else if (isUnknown()) {
throw new ExplicitException("Unknown user does not have git identity");
} else if (isSystem()) {
return new PersonIdent(User.SYSTEM_NAME, User.SYSTEM_EMAIL_ADDRESS);
} else {
EmailAddress emailAddress = getGitEmailAddress();
@ -708,6 +746,38 @@ public class User extends AbstractEntity implements AuthenticationInfo {
this.projectAuthorizations = projectAuthorizations;
}
public Collection<ProjectEntitlement> getProjectEntitlements() {
return projectEntitlements;
}
public void setProjectEntitlements(Collection<ProjectEntitlement> projectEntitlements) {
this.projectEntitlements = projectEntitlements;
}
public Collection<GroupEntitlement> getGroupEntitlements() {
return groupEntitlements;
}
public void setGroupEntitlements(Collection<GroupEntitlement> groupEntitlements) {
this.groupEntitlements = groupEntitlements;
}
public Collection<UserEntitlement> getUserEntitlements() {
return userEntitlements;
}
public void setUserEntitlements(Collection<UserEntitlement> userEntitlements) {
this.userEntitlements = userEntitlements;
}
public Collection<UserEntitlement> getAiEntitlements() {
return aiEntitlements;
}
public void setAiEntitlements(Collection<UserEntitlement> aiEntitlements) {
this.aiEntitlements = aiEntitlements;
}
public Collection<IssueAuthorization> getIssueAuthorizations() {
return issueAuthorizations;
}
@ -1106,8 +1176,37 @@ public class User extends AbstractEntity implements AuthenticationInfo {
getEmailAddresses().add(emailAddress);
}
public List<User> getEntitledAis() {
if (entitledAis == null) {
var userService = OneDev.getInstance(UserService.class);
var userCache = userService.cloneCache();
var aiUserFacades = userCache.values().stream()
.filter(it -> it.getType() == AI && !it.isDisabled() && !it.getId().equals(getId()))
.collect(Collectors.toList());
if (aiUserFacades.stream().allMatch(it->it.isEntitleToAll())) {
entitledAis = aiUserFacades.stream()
.sorted(Comparator.comparing(UserFacade::getDisplayName))
.map(it -> userService.load(it.getId()))
.collect(Collectors.toList());
} else {
var entitledAiSet = aiUserFacades.stream()
.filter(it->it.isEntitleToAll())
.map(it -> userService.load(it.getId()))
.collect(Collectors.toSet());
getAiEntitlements().stream().forEach(it -> entitledAiSet.add(it.getAI()));
for (var group: getGroups()) {
group.getEntitlements().stream().forEach(it -> entitledAiSet.add(it.getAi()));
}
entitledAis = new ArrayList<User>(entitledAiSet);
entitledAis.sort(Comparator.comparing(User::getDisplayName));
}
}
return entitledAis;
}
public UserFacade getFacade() {
return new UserFacade(getId(), getName(), getFullName(), isServiceAccount(), isDisabled());
return new UserFacade(getId(), getName(), getFullName(), getType(), isDisabled(), getAiSetting().isEntitleToAll());
}
}

View File

@ -0,0 +1,52 @@
package io.onedev.server.model;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Table(
indexes={@Index(columnList="o_ai_id"), @Index(columnList="o_user_id")},
uniqueConstraints={@UniqueConstraint(columnNames={"o_ai_id", "o_user_id"})
})
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class UserEntitlement extends AbstractEntity {
private static final long serialVersionUID = 1L;
public static final String PROP_AI = "ai";
public static final String PROP_USER = "user";
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private User ai;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private User user;
public User getAI() {
return ai;
}
public void setAI(User ai) {
this.ai = ai;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}

View File

@ -19,19 +19,21 @@ import org.slf4j.LoggerFactory;
import com.google.gson.JsonParser;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import io.onedev.server.annotation.ChoiceProvider;
import io.onedev.server.annotation.Editable;
import io.onedev.server.util.EditContext;
@Editable(order=100)
public class AIModelSetting implements Serializable {
public class AiModelSetting implements Serializable {
private static final long serialVersionUID = 1L;
private static final int TIMEOUT_SECONDS = 30;
private static final Logger logger = LoggerFactory.getLogger(AIModelSetting.class);
private static final Logger logger = LoggerFactory.getLogger(AiModelSetting.class);
private String baseUrl;
@ -128,4 +130,13 @@ public class AIModelSetting implements Serializable {
.build();
}
public StreamingChatModel getStreamingChatModel() {
return OpenAiStreamingChatModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(name)
.timeout(Duration.ofSeconds(TIMEOUT_SECONDS))
.build();
}
}

View File

@ -0,0 +1,29 @@
package io.onedev.server.model.support;
import java.io.Serializable;
public class AiSetting implements Serializable {
private static final long serialVersionUID = 1L;
private AiModelSetting modelSetting;
private boolean entitleToAll = true;
public AiModelSetting getModelSetting() {
return modelSetting;
}
public void setModelSetting(AiModelSetting modelSetting) {
this.modelSetting = modelSetting;
}
public boolean isEntitleToAll() {
return entitleToAll;
}
public void setEntitleToAll(boolean entitleToAll) {
this.entitleToAll = entitleToAll;
}
}

View File

@ -6,24 +6,24 @@ import org.jspecify.annotations.Nullable;
import dev.langchain4j.model.chat.ChatModel;
import io.onedev.server.annotation.Editable;
import io.onedev.server.model.support.AIModelSetting;
import io.onedev.server.model.support.AiModelSetting;
@Editable
public class AISetting implements Serializable {
public class AiSetting implements Serializable {
private static final long serialVersionUID = 1L;
public static final String PROP_LITE_MODEL_SETTING = "liteModelSetting";
private AIModelSetting liteModelSetting;
private AiModelSetting liteModelSetting;
@Editable(order=100)
@Nullable
public AIModelSetting getLiteModelSetting() {
public AiModelSetting getLiteModelSetting() {
return liteModelSetting;
}
public void setLiteModelSetting(AIModelSetting liteModelSetting) {
public void setLiteModelSetting(AiModelSetting liteModelSetting) {
this.liteModelSetting = liteModelSetting;
}

View File

@ -205,13 +205,13 @@ public class GlobalIssueSetting implements Serializable {
var branchUpdatedSpec = new BranchUpdatedSpec();
branchUpdatedSpec.setToState("Closed");
branchUpdatedSpec.setBranches("main");
branchUpdatedSpec.setBranches("main master");
branchUpdatedSpec.setIssueQuery("fixed in current commit");
transitionSpecs.add(branchUpdatedSpec);
var pullRequestOpenedSpec = new PullRequestOpenedSpec();
pullRequestOpenedSpec.setToState("In Review");
pullRequestOpenedSpec.setBranches("main");
pullRequestOpenedSpec.setBranches("main master");
pullRequestOpenedSpec.setIssueQuery("fixed in current pull request");
transitionSpecs.add(pullRequestOpenedSpec);

View File

@ -107,7 +107,7 @@ public class BuildNotificationManager {
@Sessional
@Listen
public void on(BuildEvent event) {
if (!(event instanceof BuildUpdated) && (event.getUser() == null || !event.getUser().isServiceAccount())) {
if (!(event instanceof BuildUpdated) && (event.getUser() == null || event.getUser().getType() != User.Type.SERVICE)) {
Project project = event.getProject();
Map<User, Collection<String>> subscribedQueryStrings = new HashMap<>();
for (BuildQueryPersonalization personalization: project.getBuildQueryPersonalizations()) {

View File

@ -72,7 +72,7 @@ public abstract class ChannelNotificationManager<T extends ChannelNotificationSe
@Sessional
@Listen
public void on(PullRequestEvent event) {
if (!event.isMinor() && (event.getUser() == null || !event.getUser().isServiceAccount())) {
if (!event.isMinor() && (event.getUser() == null || event.getUser().getType() != User.Type.SERVICE)) {
PullRequest request = event.getRequest();
User user = event.getUser();
@ -91,7 +91,7 @@ public abstract class ChannelNotificationManager<T extends ChannelNotificationSe
@Sessional
@Listen
public void on(BuildEvent event) {
if (event.getUser() == null || !event.getUser().isServiceAccount()) {
if (event.getUser() == null || event.getUser().getType() != User.Type.SERVICE) {
Build build = event.getBuild();
var status = StringUtils.capitalize(build.getStatus().toString().toLowerCase());
@ -107,7 +107,7 @@ public abstract class ChannelNotificationManager<T extends ChannelNotificationSe
@Sessional
@Listen
public void on(PackEvent event) {
if (!event.getUser().isServiceAccount()) {
if (event.getUser().getType() != User.Type.SERVICE) {
Pack pack = event.getPack();
var title = format("[%s %s] Package published", pack.getType(), pack.getReference(true));
postIfApplicable(title, event);
@ -134,7 +134,7 @@ public abstract class ChannelNotificationManager<T extends ChannelNotificationSe
@Sessional
@Listen
public void on(CodeCommentEvent event) {
if (!(event instanceof CodeCommentEdited) && !event.getUser().isServiceAccount()) {
if (!(event instanceof CodeCommentEdited) && event.getUser().getType() != User.Type.SERVICE) {
CodeComment comment = event.getComment();
String title = format("[Code Comment %s:%s] %s %s",

View File

@ -41,7 +41,7 @@ public class CodeCommentNotificationManager {
@Listen
public void on(CodeCommentEvent event) {
CodeComment comment = event.getComment();
if (comment.getCompareContext().getPullRequest() == null && !event.getUser().isServiceAccount()) {
if (comment.getCompareContext().getPullRequest() == null && event.getUser().getType() != User.Type.SERVICE) {
MarkdownText markdown = (MarkdownText) event.getCommentText();
Collection<User> notifyUsers = new HashSet<>();

View File

@ -177,7 +177,7 @@ public class IssueNotificationManager {
if (user != null) {
if (!user.isNotifyOwnEvents() || isNotified(notifiedEmailAddresses, user))
notifiedUsers.add(user);
if (!user.isSystem() && !user.isServiceAccount())
if (!user.isSystem() && user.getType() != User.Type.SERVICE)
watchService.watch(issue, user, true);
}
@ -208,7 +208,7 @@ public class IssueNotificationManager {
}
for (User member: entry.getValue().getMembers()) {
if (!member.isServiceAccount())
if (member.getType() != User.Type.SERVICE)
watchService.watch(issue, member, true);
authorizationService.authorize(issue, member);
}
@ -237,7 +237,7 @@ public class IssueNotificationManager {
}
for (User each: entry.getValue()) {
if (!each.isServiceAccount())
if (each.getType() != User.Type.SERVICE)
watchService.watch(issue, each, true);
authorizationService.authorize(issue, each);
}
@ -250,7 +250,7 @@ public class IssueNotificationManager {
User mentionedUser = userService.findByName(userName);
if (mentionedUser != null) {
mentionService.mention(issue, mentionedUser);
if (!mentionedUser.isServiceAccount())
if (mentionedUser.getType() != User.Type.SERVICE)
watchService.watch(issue, mentionedUser, true);
authorizationService.authorize(issue, mentionedUser);
if (!isNotified(notifiedEmailAddresses, mentionedUser)) {

View File

@ -83,7 +83,7 @@ public class PackNotificationManager {
@Sessional
@Listen
public void on(PackEvent event) {
if (!event.getUser().isServiceAccount()) {
if (event.getUser().getType() != User.Type.SERVICE) {
Project project = event.getProject();
Map<User, Collection<String>> subscribedQueryStrings = new HashMap<>();
for (PackQueryPersonalization personalization: project.getPackQueryPersonalizations()) {

View File

@ -63,7 +63,7 @@ public class PullRequestNotificationManager {
@Transactional
@Listen
public void on(PullRequestEvent event) {
if (event.getUser() == null || !event.getUser().isServiceAccount()) {
if (event.getUser() == null || event.getUser().getType() != User.Type.SERVICE) {
PullRequest request = event.getRequest();
User user = event.getUser();
@ -131,7 +131,7 @@ public class PullRequestNotificationManager {
if (user != null) {
if (!user.isNotifyOwnEvents() || isNotified(notifiedEmailAddresses, user))
notifiedUsers.add(user);
if (!user.isSystem() && !user.isServiceAccount())
if (!user.isSystem() && user.getType() != User.Type.SERVICE)
watchService.watch(request, user, true);
}
@ -144,7 +144,7 @@ public class PullRequestNotificationManager {
notifiedUsers.add(committer);
}
for (User each : committers) {
if (!each.isSystem() && !each.isServiceAccount())
if (!each.isSystem() && each.getType() != User.Type.SERVICE)
watchService.watch(request, each, true);
}
}
@ -206,7 +206,7 @@ public class PullRequestNotificationManager {
}
for (User assignee : assignees) {
if (!assignee.isServiceAccount())
if (assignee.getType() != User.Type.SERVICE)
watchService.watch(request, assignee, true);
if (!notifiedUsers.contains(assignee)) {
String subject = String.format(
@ -232,7 +232,7 @@ public class PullRequestNotificationManager {
}
for (User reviewer : reviewers) {
if (!reviewer.isServiceAccount())
if (reviewer.getType() != User.Type.SERVICE)
watchService.watch(request, reviewer, true);
if (!notifiedUsers.contains(reviewer)) {
String subject = String.format(
@ -264,7 +264,7 @@ public class PullRequestNotificationManager {
User mentionedUser = userService.findByName(userName);
if (mentionedUser != null) {
mentionService.mention(request, mentionedUser);
if (!mentionedUser.isServiceAccount())
if (mentionedUser.getType() != User.Type.SERVICE)
watchService.watch(request, mentionedUser, true);
if (!isNotified(notifiedEmailAddresses, mentionedUser)) {
String subject = String.format(

View File

@ -19,12 +19,13 @@ import javax.ws.rs.core.Response;
import org.apache.shiro.authz.UnauthorizedException;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.model.EmailAddress;
import io.onedev.server.model.User;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.service.AuditService;
import io.onedev.server.service.EmailAddressService;
import io.onedev.server.service.SettingService;
import io.onedev.server.model.EmailAddress;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.security.SecurityUtils;
@Path("/email-addresses")
@Consumes(MediaType.APPLICATION_JSON)
@ -73,8 +74,8 @@ public class EmailAddressResource {
throw new UnauthorizedException();
else if (owner.isDisabled())
throw new ExplicitException("Can not set email address for disabled user");
else if (owner.isServiceAccount())
throw new ExplicitException("Can not set email address for service account");
else if (owner.getType() != User.Type.ORDINARY)
throw new ExplicitException("Can not set email address for service or ai user");
else if (emailAddressService.findByValue(emailAddress.getValue()) != null)
throw new ExplicitException("This email address is already used by another user");

View File

@ -1,5 +1,7 @@
package io.onedev.server.rest.resource;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.model.User.Type.SERVICE;
import static io.onedev.server.security.SecurityUtils.getAuthUser;
import static java.util.stream.Collectors.toList;
@ -101,10 +103,10 @@ public class UserResource {
private UserData getData(User user) {
var data = new UserData();
data.setDisabled(user.isDisabled());
data.setServiceAccount(user.isServiceAccount());
data.setType(user.getType());
data.setName(user.getName());
data.setFullName(user.getFullName());
if (!user.isServiceAccount())
if (user.getType() != SERVICE)
data.setNotifyOwnEvents(user.isNotifyOwnEvents());
return data;
}
@ -333,14 +335,14 @@ public class UserResource {
if (userService.findByName(data.getName()) != null)
throw new ExplicitException("Login name is already used by another user");
if (!data.isServiceAccount() && emailAddressService.findByValue(data.getEmailAddress()) != null)
if (data.getType() == ORDINARY && emailAddressService.findByValue(data.getEmailAddress()) != null)
throw new ExplicitException("Email address is already used by another user");
User user = new User();
user.setServiceAccount(data.isServiceAccount());
user.setType(data.getType());
user.setName(data.getName());
user.setFullName(data.getFullName());
if (data.isServiceAccount()) {
if (data.getType() != ORDINARY) {
userService.create(user);
} else {
user.setNotifyOwnEvents(data.isNotifyOwnEvents());
@ -383,7 +385,7 @@ public class UserResource {
String oldName = user.getName();
user.setName(data.getName());
user.setFullName(data.getFullName());
if (!user.isServiceAccount())
if (user.getType() != SERVICE)
user.setNotifyOwnEvents(data.isNotifyOwnEvents());
userService.update(user, oldName);
@ -462,9 +464,9 @@ public class UserResource {
auditService.audit(null, "changed password of account \"" + user.getName() + "\" via RESTful API", null, null);
return Response.ok().build();
} else if (user.isDisabled()) {
throw new ExplicitException("Can not set password for disabled user");
} else if (user.isServiceAccount()) {
throw new ExplicitException("Can not set password for service account");
throw new ExplicitException("Can not set password for disabled account");
} else if (user.getType() != ORDINARY) {
throw new ExplicitException("Can not set password for service or AI account");
} else if (user.equals(getAuthUser())) {
if (user.getPassword() == null) {
throw new ExplicitException("The user is currently authenticated via external system, "
@ -488,9 +490,9 @@ public class UserResource {
User user = userService.load(userId);
if (user.isDisabled()) {
throw new ExplicitException("Can not reset two factor authentication for disabled user");
} else if (user.isServiceAccount()) {
throw new ExplicitException("Can not reset two factor authentication for service account");
throw new ExplicitException("Can not reset two factor authentication for disabled account");
} else if (user.getType() != ORDINARY) {
throw new ExplicitException("Can not reset two factor authentication for service or AI account");
} else {
user.setTwoFactorAuthentication(null);
userService.update(user, null);
@ -509,8 +511,8 @@ public class UserResource {
if (user.isDisabled())
throw new ExplicitException("Can not set queries and watches for disabled user");
else if (user.isServiceAccount())
throw new ExplicitException("Can not set queries and watches for service account");
else if (user.getType() != ORDINARY)
throw new ExplicitException("Can not set queries and watches for service or ai account");
var oldAuditContent = VersionedXmlDoc.fromBean(getQueriesAndWatches(user)).toXML();
@ -587,8 +589,8 @@ public class UserResource {
@Api(order=10, description="Whether or not the user is disabled")
private boolean disabled;
@Api(order=50, description="Whether or not the user is a service account")
private boolean serviceAccount;
@Api(order=50, description="Type of the user")
private User.Type type;
@Api(order=100, description="Login name of the user")
private String name;
@ -607,12 +609,12 @@ public class UserResource {
this.disabled = disabled;
}
public boolean isServiceAccount() {
return serviceAccount;
public User.Type getType() {
return type;
}
public void setServiceAccount(boolean serviceAccount) {
this.serviceAccount = serviceAccount;
public void setType(User.Type type) {
this.type = type;
}
public String getName() {
@ -645,8 +647,8 @@ public class UserResource {
private static final long serialVersionUID = 1L;
@Api(order=50, exampleProvider="getServiceAccountExample", description="Create user as service account")
private boolean serviceAccount;
@Api(order=50, exampleProvider = "getTypeExample", description="Specify account type")
private User.Type type;
@Api(order=100, description="Login name of the user")
private String name;
@ -662,17 +664,17 @@ public class UserResource {
@Api(order=400, description = "Whether or not to notify user on own events. Only required if not created as service account")
private boolean notifyOwnEvents;
public boolean isServiceAccount() {
return serviceAccount;
public User.Type getType() {
return type;
}
public void setServiceAccount(boolean serviceAccount) {
this.serviceAccount = serviceAccount;
public void setType(User.Type type) {
this.type = type;
}
@SuppressWarnings("unused")
private static boolean getServiceAccountExample() {
return false;
private static User.Type getTypeExample() {
return ORDINARY;
}
@UserName

View File

@ -1,5 +1,6 @@
package io.onedev.server.security.realm;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.validation.validator.UserNameValidator.normalizeUserName;
import static io.onedev.server.web.translation.Translation._T;
@ -145,8 +146,8 @@ public class PasswordAuthenticatingRealm extends AuthenticatingRealm {
if (user != null) {
if (user.isDisabled())
throw new DisabledAccountException(_T("Account is disabled"));
else if (user.isServiceAccount())
throw new DisabledAccountException(_T("Service account not allowed to login"));
else if (user.getType() != ORDINARY)
throw new DisabledAccountException(_T("Service or AI account not allowed to login"));
if (user.getPassword() == null) {
var authenticator = settingService.getAuthenticator();
if (authenticator != null) {

View File

@ -0,0 +1,23 @@
package io.onedev.server.service;
import java.util.List;
import org.jspecify.annotations.Nullable;
import io.onedev.server.model.Chat;
import io.onedev.server.model.ChatMessage;
import io.onedev.server.model.User;
import io.onedev.server.service.support.ChatResponding;
public interface ChatService extends EntityService<Chat> {
List<Chat> query(User user, User ai, String term, int count);
void createOrUpdate(Chat chat);
void sendRequest(String sessionId, ChatMessage request);
@Nullable
ChatResponding getResponding(String sessionId, Chat chat);
}

View File

@ -0,0 +1,14 @@
package io.onedev.server.service;
import java.util.Collection;
import io.onedev.server.model.GroupEntitlement;
import io.onedev.server.model.User;
public interface GroupEntitlementService extends EntityService<GroupEntitlement> {
void syncEntitlements(User ai, Collection<GroupEntitlement> entitlements);
void create(GroupEntitlement entitlement);
}

View File

@ -0,0 +1,14 @@
package io.onedev.server.service;
import java.util.Collection;
import io.onedev.server.model.ProjectEntitlement;
import io.onedev.server.model.User;
public interface ProjectEntitlementService extends EntityService<ProjectEntitlement> {
void syncEntitlements(User ai, Collection<ProjectEntitlement> entitlements);
void create(ProjectEntitlement entitlement);
}

View File

@ -9,7 +9,7 @@ import org.jspecify.annotations.Nullable;
import io.onedev.server.annotation.NoDBAccess;
import io.onedev.server.model.Setting;
import io.onedev.server.model.Setting.Key;
import io.onedev.server.model.support.administration.AISetting;
import io.onedev.server.model.support.administration.AiSetting;
import io.onedev.server.model.support.administration.AgentSetting;
import io.onedev.server.model.support.administration.AlertSetting;
import io.onedev.server.model.support.administration.AuditSetting;
@ -173,9 +173,9 @@ public interface SettingService extends EntityService<Setting> {
PerformanceSetting getPerformanceSetting();
AISetting getAISetting();
AiSetting getAiSetting();
void saveAISetting(AISetting aiSetting);
void saveAiSetting(AiSetting aiSetting);
void saveSshSetting(SshSetting sshSetting);

View File

@ -0,0 +1,14 @@
package io.onedev.server.service;
import java.util.Collection;
import io.onedev.server.model.User;
import io.onedev.server.model.UserEntitlement;
public interface UserEntitlementService extends EntityService<UserEntitlement> {
void syncEntitlements(User ai, Collection<UserEntitlement> entitlements);
void create(UserEntitlement entitlement);
}

View File

@ -0,0 +1,293 @@
package io.onedev.server.service.impl;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.PartialResponse;
import dev.langchain4j.model.chat.response.PartialResponseContext;
import dev.langchain4j.model.chat.response.PartialThinking;
import dev.langchain4j.model.chat.response.PartialThinkingContext;
import dev.langchain4j.model.chat.response.PartialToolCall;
import dev.langchain4j.model.chat.response.PartialToolCallContext;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.exception.ExceptionUtils;
import io.onedev.server.model.Chat;
import io.onedev.server.model.ChatMessage;
import io.onedev.server.model.User;
import io.onedev.server.persistence.TransactionService;
import io.onedev.server.persistence.annotation.Sessional;
import io.onedev.server.persistence.annotation.Transactional;
import io.onedev.server.persistence.dao.EntityCriteria;
import io.onedev.server.service.ChatService;
import io.onedev.server.service.support.ChatResponding;
import io.onedev.server.web.SessionListener;
import io.onedev.server.web.websocket.WebSocketService;
@Singleton
public class DefaultChatService extends BaseEntityService<Chat> implements ChatService, SessionListener {
private static final Logger logger = LoggerFactory.getLogger(DefaultChatService.class);
private static final int MAX_HISTORY_MESSAGES = 25;
private static final int MAX_HISTORY_MESSAGE_LEN = 1024;
private static final int PARTIAL_RESPONSE_NOTIFICATION_INTERVAL = 1000;
@Inject
private ExecutorService executorService;
@Inject
private TransactionService transactionService;
@Inject
private WebSocketService webSocketService;
private final Map<String, Map<Long, ChatRespondingImpl>> respondings = new ConcurrentHashMap<>();
@Override
public List<Chat> query(User user, User ai, String term, int count) {
EntityCriteria<Chat> criteria = EntityCriteria.of(Chat.class);
criteria.add(Restrictions.eq(Chat.PROP_USER, user));
criteria.add(Restrictions.eq(Chat.PROP_AI, ai));
criteria.add(Restrictions.ilike(Chat.PROP_TITLE, "%" + term + "%"));
criteria.addOrder(Order.desc(Chat.PROP_DATE));
return query(criteria);
}
@Override
public void createOrUpdate(Chat chat) {
dao.persist(chat);
}
@Sessional
@Override
public ChatResponding getResponding(String sessionId, Chat chat) {
return getResponding(sessionId, chat.getId());
}
@Transactional
@Override
public void sendRequest(String sessionId, ChatMessage request) {
var requestId = request.getId();
var chatId = request.getChat().getId();
var modelSetting = request.getChat().getAi().getAiSetting().getModelSetting();
var messages = request.getChat().getSortedMessages()
.stream()
.filter(it->!it.isError())
.collect(Collectors.toList());
if (messages.size() > MAX_HISTORY_MESSAGES)
messages = messages.subList(messages.size()-MAX_HISTORY_MESSAGES, messages.size());
var langchain4jMessages = messages.stream()
.map(it -> {
var content = it.getContent();
if (!it.equals(request))
content = StringUtils.abbreviate(content, MAX_HISTORY_MESSAGE_LEN);
if (it.isRequest())
return new UserMessage(content);
else
return new AiMessage(content);
})
.collect(Collectors.toList());
var future = executorService.submit(() -> {
var latch = new CountDownLatch(1);
var handler = new StreamingChatResponseHandler() {
private long lastPartialResponseNotificationTime = System.currentTimeMillis();
@Override
public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {
if (latch.getCount() == 0) {
context.streamingHandle().cancel();
} else {
var responding = getResponding(sessionId, chatId, requestId);
if (responding != null) {
var content = responding.getContent();
if (content == null)
content = "";
content += partialResponse.text();
responding.content = content;
if (System.currentTimeMillis() - lastPartialResponseNotificationTime > PARTIAL_RESPONSE_NOTIFICATION_INTERVAL) {
lastPartialResponseNotificationTime = System.currentTimeMillis();
webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null);
}
}
}
}
@Override
public void onPartialThinking(PartialThinking partialThinking, PartialThinkingContext context) {
if (latch.getCount() == 0)
context.streamingHandle().cancel();
}
@Override
public void onPartialToolCall(PartialToolCall partialToolCall, PartialToolCallContext context) {
if (latch.getCount() == 0)
context.streamingHandle().cancel();
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
try {
createResponseIfNecessary(sessionId, chatId, requestId, completeResponse.aiMessage().text(), null);
} finally {
latch.countDown();
}
}
@Override
public void onError(Throwable error) {
try {
createResponseIfNecessary(sessionId, chatId, requestId, "Error getting chat response, check server log for details", error);
} finally {
latch.countDown();
}
}
};
try {
modelSetting.getStreamingChatModel().chat(langchain4jMessages, handler);
transactionService.run(() -> {
var chat = load(chatId);
var requests = chat.getMessages().stream().filter(it->it.isRequest()).collect(Collectors.toList());
if (requests.size() == 1) {
var systemPrompt = String.format("""
Summarize provided message to get a compact title with below requirements:
1. It should be within %d characters
2. Only title is returned, no other text or comments
""", Chat.MAX_TITLE_LEN);
var title = modelSetting.getChatModel().chat(new SystemMessage(systemPrompt), new UserMessage(requests.get(0).getContent())).aiMessage().text();
chat.setTitle(title);
webSocketService.notifyObservableChange(Chat.getChangeObservable(chatId), null);
}
});
latch.await();
} catch (Exception e) {
if (ExceptionUtils.find(e, InterruptedException.class) == null) {
logger.error("Error getting chat response", e);
String errorMessage = e.getMessage();
if (errorMessage == null)
errorMessage = "Error getting chat response, check server log for details";
createResponseIfNecessary(sessionId, chatId, requestId, errorMessage, e);
} else {
var responding = getResponding(sessionId, chatId, requestId);
if (responding != null && responding.getContent() != null)
createResponseIfNecessary(sessionId, chatId, requestId, responding.getContent(), null);
}
} finally {
latch.countDown();
var respondingsOfSession = respondings.get(sessionId);
if (respondingsOfSession != null) {
var responding = respondingsOfSession.get(chatId);
if (responding != null) {
respondingsOfSession.computeIfPresent(chatId, (k, v)-> {
if (v.requestId.equals(requestId))
return null;
else
return v;
});
webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null);
}
}
}
});
var previousResponding = respondings.computeIfAbsent(sessionId, it->new ConcurrentHashMap<>()).put(chatId, new ChatRespondingImpl(requestId, future));
if (previousResponding != null)
previousResponding.cancel();
}
private ChatRespondingImpl getResponding(String sessionId, Long chatId) {
var respondingsOfSession = respondings.get(sessionId);
if (respondingsOfSession != null) {
return respondingsOfSession.get(chatId);
}
return null;
}
private ChatRespondingImpl getResponding(String sessionId, Long chatId, Long requestId) {
var responding = getResponding(sessionId, chatId);
if (responding != null && responding.requestId.equals(requestId))
return responding;
else
return null;
}
private void createResponseIfNecessary(String sessionId, Long chatId, Long requestId, String content, @Nullable Throwable throwable) {
var responding = getResponding(sessionId, chatId, requestId);
if (responding != null) {
transactionService.run(() -> {
var chat = load(chatId);
if (throwable != null)
logger.error("Error getting chat response", throwable);
var response = new ChatMessage();
response.setChat(chat);
response.setError(throwable != null);
response.setContent(content);
dao.persist(response);
webSocketService.notifyObservableChange(Chat.getNewMessagesObservable(chatId), null);
});
};
}
@Override
public void sessionCreated(String sessionId) {
}
@Override
public void sessionDestroyed(String sessionId) {
var respondingsOfSession = respondings.remove(sessionId);
if (respondingsOfSession != null) {
for (var responding : respondingsOfSession.values())
responding.cancel();
}
}
private static class ChatRespondingImpl implements ChatResponding {
private final Long requestId;
private final Future<?> future;
private volatile String content;
ChatRespondingImpl(Long requestId, Future<?> future) {
this.requestId = requestId;
this.future = future;
}
@Override
public String getContent() {
return content;
}
@Override
public void cancel() {
future.cancel(true);
}
}
}

View File

@ -0,0 +1,63 @@
package io.onedev.server.service.impl;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Singleton;
import com.google.common.base.Preconditions;
import io.onedev.server.model.GroupEntitlement;
import io.onedev.server.model.User;
import io.onedev.server.persistence.annotation.Transactional;
import io.onedev.server.service.GroupEntitlementService;
@Singleton
public class DefaultGroupEntitlementService extends BaseEntityService<GroupEntitlement>
implements GroupEntitlementService {
@Override
public List<GroupEntitlement> query() {
return query(true);
}
@Override
public int count() {
return count(true);
}
@Transactional
@Override
public void syncEntitlements(User ai, Collection<GroupEntitlement> entitlements) {
var newGroups = entitlements.stream()
.map(GroupEntitlement::getGroup)
.collect(Collectors.toSet());
ai.getGroupEntitlements().removeIf(it -> {
if (!newGroups.contains(it.getGroup())) {
delete(it);
return true;
}
return false;
});
var existingGroups = ai.getGroupEntitlements().stream()
.map(GroupEntitlement::getGroup)
.collect(Collectors.toSet());
entitlements.stream()
.filter(it -> !existingGroups.contains(it.getGroup()))
.forEach(it -> {
ai.getGroupEntitlements().add(it);
dao.persist(it);
});
}
@Transactional
@Override
public void create(GroupEntitlement entitlement) {
Preconditions.checkState(entitlement.isNew());
dao.persist(entitlement);
}
}

View File

@ -1,5 +1,6 @@
package io.onedev.server.service.impl;
import static io.onedev.server.model.User.Type.SERVICE;
import static java.lang.Integer.MAX_VALUE;
import java.util.ArrayList;
@ -170,7 +171,7 @@ public class DefaultIssueChangeService extends BaseEntityService<IssueChange>
@Transactional
@Override
public void create(IssueChange change, @Nullable String note) {
create(change, note, !change.getUser().isServiceAccount());
create(change, note, change.getUser().getType() != SERVICE);
}
@Transactional
@ -317,7 +318,7 @@ public class DefaultIssueChangeService extends BaseEntityService<IssueChange>
@Transactional
@Override
public void addSchedule(User user, Issue issue, Iteration iteration) {
addSchedule(user, issue, iteration, !user.isServiceAccount());
addSchedule(user, issue, iteration, user.getType() != SERVICE);
}
protected void addSchedule(User user, Issue issue, Iteration iteration, boolean sendNotifications) {
@ -348,7 +349,7 @@ public class DefaultIssueChangeService extends BaseEntityService<IssueChange>
@Transactional
@Override
public void removeSchedule(User user, Issue issue, Iteration iteration) {
removeSchedule(user, issue, iteration, !user.isServiceAccount());
removeSchedule(user, issue, iteration, user.getType() != SERVICE);
}
@Transactional

View File

@ -0,0 +1,63 @@
package io.onedev.server.service.impl;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Singleton;
import com.google.common.base.Preconditions;
import io.onedev.server.model.ProjectEntitlement;
import io.onedev.server.model.User;
import io.onedev.server.persistence.annotation.Transactional;
import io.onedev.server.service.ProjectEntitlementService;
@Singleton
public class DefaultProjectEntitlementService extends BaseEntityService<ProjectEntitlement>
implements ProjectEntitlementService {
@Override
public List<ProjectEntitlement> query() {
return query(true);
}
@Override
public int count() {
return count(true);
}
@Transactional
@Override
public void syncEntitlements(User ai, Collection<ProjectEntitlement> entitlements) {
var newProjects = entitlements.stream()
.map(ProjectEntitlement::getProject)
.collect(Collectors.toSet());
ai.getProjectEntitlements().removeIf(it -> {
if (!newProjects.contains(it.getProject())) {
delete(it);
return true;
}
return false;
});
var existingProjects = ai.getProjectEntitlements().stream()
.map(ProjectEntitlement::getProject)
.collect(Collectors.toSet());
entitlements.stream()
.filter(it -> !existingProjects.contains(it.getProject()))
.forEach(it -> {
ai.getProjectEntitlements().add(it);
dao.persist(it);
});
}
@Transactional
@Override
public void create(ProjectEntitlement entitlement) {
Preconditions.checkState(entitlement.isNew());
dao.persist(entitlement);
}
}

View File

@ -30,7 +30,7 @@ import io.onedev.server.event.entity.EntityPersisted;
import io.onedev.server.event.system.SystemStarting;
import io.onedev.server.model.Setting;
import io.onedev.server.model.Setting.Key;
import io.onedev.server.model.support.administration.AISetting;
import io.onedev.server.model.support.administration.AiSetting;
import io.onedev.server.model.support.administration.AgentSetting;
import io.onedev.server.model.support.administration.AlertSetting;
import io.onedev.server.model.support.administration.AuditSetting;
@ -168,8 +168,8 @@ public class DefaultSettingService extends BaseEntityService<Setting> implements
}
@Override
public AISetting getAISetting() {
return (AISetting) getSettingValue(Key.AI);
public AiSetting getAiSetting() {
return (AiSetting) getSettingValue(Key.AI);
}
@Override
@ -304,7 +304,7 @@ public class DefaultSettingService extends BaseEntityService<Setting> implements
@Transactional
@Override
public void saveAISetting(AISetting aiSetting) {
public void saveAiSetting(AiSetting aiSetting) {
saveSetting(Key.AI, aiSetting);
}

View File

@ -0,0 +1,63 @@
package io.onedev.server.service.impl;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Singleton;
import com.google.common.base.Preconditions;
import io.onedev.server.model.User;
import io.onedev.server.model.UserEntitlement;
import io.onedev.server.persistence.annotation.Transactional;
import io.onedev.server.service.UserEntitlementService;
@Singleton
public class DefaultUserEntitlementService extends BaseEntityService<UserEntitlement>
implements UserEntitlementService {
@Override
public List<UserEntitlement> query() {
return query(true);
}
@Override
public int count() {
return count(true);
}
@Transactional
@Override
public void syncEntitlements(User ai, Collection<UserEntitlement> entitlements) {
var newUsers = entitlements.stream()
.map(UserEntitlement::getUser)
.collect(Collectors.toSet());
ai.getUserEntitlements().removeIf(it -> {
if (!newUsers.contains(it.getUser())) {
delete(it);
return true;
}
return false;
});
var existingUsers = ai.getUserEntitlements().stream()
.map(UserEntitlement::getUser)
.collect(Collectors.toSet());
entitlements.stream()
.filter(it -> !existingUsers.contains(it.getUser()))
.forEach(it -> {
ai.getUserEntitlements().add(it);
dao.persist(it);
});
}
@Transactional
@Override
public void create(UserEntitlement entitlement) {
Preconditions.checkState(entitlement.isNew());
dao.persist(entitlement);
}
}

View File

@ -1,5 +1,7 @@
package io.onedev.server.service.impl;
import static io.onedev.server.model.User.Type.SERVICE;
import java.util.Collection;
import java.util.List;
@ -18,13 +20,11 @@ import org.hibernate.query.Query;
import com.google.common.base.Preconditions;
import com.hazelcast.core.HazelcastInstance;
import io.onedev.server.SubscriptionService;
import io.onedev.server.cluster.ClusterService;
import io.onedev.server.event.Listen;
import io.onedev.server.event.entity.EntityPersisted;
import io.onedev.server.event.entity.EntityRemoved;
import io.onedev.server.event.system.SystemStarting;
import io.onedev.server.exception.NoSubscriptionException;
import io.onedev.server.model.AbstractEntity;
import io.onedev.server.model.EmailAddress;
import io.onedev.server.model.Project;
@ -68,9 +68,6 @@ public class DefaultUserService extends BaseEntityService<User> implements UserS
@Inject
private ClusterService clusterService;
@Inject
private SubscriptionService subscriptionService;
private volatile UserCache cache;
@ -115,8 +112,6 @@ public class DefaultUserService extends BaseEntityService<User> implements UserS
@Override
public void create(User user) {
Preconditions.checkState(user.isNew());
if (user.isServiceAccount() && !subscriptionService.isSubscriptionActive())
throw new NoSubscriptionException("Service account");
user.setName(user.getName().toLowerCase());
dao.persist(user);
}
@ -385,7 +380,7 @@ public class DefaultUserService extends BaseEntityService<User> implements UserS
user.setPassword(null);
user.setPasswordResetCode(null);
user.setServiceAccount(true);
user.setType(SERVICE);
dao.persist(user);
}

View File

@ -0,0 +1,12 @@
package io.onedev.server.service.support;
import org.jspecify.annotations.Nullable;
public interface ChatResponding {
@Nullable
String getContent();
void cancel();
}

View File

@ -6,12 +6,14 @@ import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import io.onedev.server.OneDev;
import io.onedev.server.service.UserService;
import io.onedev.server.model.User;
import io.onedev.server.service.UserService;
import io.onedev.server.util.MapProxy;
import io.onedev.server.util.Similarities;
@ -53,19 +55,23 @@ public class UserCache extends MapProxy<Long, UserFacade> {
public UserCache clone() {
return new UserCache(new HashMap<>(delegate));
}
public Collection<User> getUsers(boolean includeDisabled) {
public Collection<User> getUsers(Function<UserFacade, Boolean> filter) {
UserService userService = OneDev.getInstance(UserService.class);
return entrySet().stream()
.filter(it -> includeDisabled || !it.getValue().isDisabled())
.filter(it -> filter.apply(it.getValue()))
.map(it -> userService.load(it.getKey()))
.collect(toSet());
}
public Collection<User> getUsers() {
return getUsers(false);
return getUsers(it->true);
}
public Comparator<User> comparingDisplayName() {
return comparingDisplayName(Set.of());
}
public Comparator<User> comparingDisplayName(Collection<User> topUsers) {
return (o1, o2) -> {
if (topUsers.contains(o1)) {

View File

@ -12,16 +12,20 @@ public class UserFacade extends EntityFacade {
private final String fullName;
private final boolean serviceAccount;
private final User.Type type;
private final boolean disabled;
private final boolean entitleToAll;
public UserFacade(Long id, String name, @Nullable String fullName, boolean serviceAccount, boolean disabled) {
public UserFacade(Long id, String name, @Nullable String fullName, User.Type type,
boolean disabled, boolean entitleToAll) {
super(id);
this.name = name;
this.fullName = fullName;
this.serviceAccount = serviceAccount;
this.type = type;
this.disabled = disabled;
this.entitleToAll = entitleToAll;
}
public String getName() {
@ -58,12 +62,16 @@ public class UserFacade extends EntityFacade {
return null;
}
public boolean isServiceAccount() {
return serviceAccount;
public User.Type getType() {
return type;
}
public boolean isDisabled() {
return disabled;
}
public boolean isEntitleToAll() {
return entitleToAll;
}
}

View File

@ -0,0 +1,9 @@
package io.onedev.server.web;
public interface SessionListener {
void sessionCreated(String sessionId);
void sessionDestroyed(String sessionId);
}

View File

@ -14,6 +14,7 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.DefaultExceptionMapper;
import org.apache.wicket.ISessionListener;
import org.apache.wicket.Page;
import org.apache.wicket.RuntimeConfigurationType;
import org.apache.wicket.Session;
@ -82,8 +83,8 @@ import io.onedev.server.web.translation.TranslationResolver;
import io.onedev.server.web.translation.TranslationStringResourceLoader;
import io.onedev.server.web.translation.TranslationTagHandler;
import io.onedev.server.web.util.AbsoluteUrlRenderer;
import io.onedev.server.web.websocket.WebSocketService;
import io.onedev.server.web.websocket.WebSocketMessages;
import io.onedev.server.web.websocket.WebSocketService;
@Singleton
public class WebApplication extends org.apache.wicket.protocol.http.WebApplication {
@ -173,6 +174,17 @@ public class WebApplication extends org.apache.wicket.protocol.http.WebApplicati
}
});
getSessionListeners().add(new ISessionListener() {
@Override
public void onCreated(Session session) {
}
@Override
public void onUnbound(String sessionId) {
}
});
getAjaxRequestTargetListeners().add(new AjaxRequestTarget.IListener() {
@Override

View File

@ -35,6 +35,12 @@ public class WebSession extends org.apache.wicket.protocol.http.WebSession {
private Map<Class<?>, String> redirectUrlsAfterDelete = new ConcurrentHashMap<>();
private Set<Long> expandedProjectIds = new ConcurrentHashSet<>();
private boolean chatVisible;
private Long activeChatId;
private String chatInput;
public WebSession(Request request) {
super(request);
@ -118,6 +124,32 @@ public class WebSession extends org.apache.wicket.protocol.http.WebSession {
this.zoneId = zoneId;
}
public boolean isChatVisible() {
return chatVisible;
}
public void setChatVisible(boolean chatVisible) {
this.chatVisible = chatVisible;
}
@Nullable
public Long getActiveChatId() {
return activeChatId;
}
public void setActiveChatId(Long activeChatId) {
this.activeChatId = activeChatId;
}
@Nullable
public String getChatInput() {
return chatInput;
}
public void setChatInput(String chatInput) {
this.chatInput = chatInput;
}
public static WebSession from(HttpSession session) {
String attributeName = "wicket:" + OneDev.getInstance(WicketServlet.class).getServletName() + ":session";
return (WebSession) session.getAttribute(attributeName);

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1763890408372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6809" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M619.776 543.936a39.552 39.552 0 0 1 21.696 21.888l144.96 362.752a34.88 34.88 0 1 1-64.832 25.92l-40.704-101.76H525.632l-41.856 102.08a34.88 34.88 0 0 1-44.8 19.328l-0.768-0.32a34.88 34.88 0 0 1-19.008-45.568l148.928-362.752a39.552 39.552 0 0 1 51.648-21.568z m229.76-3.2c19.2 0 34.88 15.616 34.816 34.88l-0.576 364.48a34.88 34.88 0 0 1-69.76-0.128l0.512-364.416c0-19.264 15.68-34.88 35.008-34.88zM378.496 55.424L459.2 121.6c21.12-3.968 38.272-6.016 52.096-6.144h1.152c13.76 0 31.36 1.92 53.12 5.824l66.368-63.36A39.552 39.552 0 0 1 678.4 51.584l0.768 0.448 114.56 66.496a39.552 39.552 0 0 1 19.392 38.784L800 269.952c9.984 13.76 17.344 25.088 22.528 34.432l0.512 0.96c4.928 9.216 9.984 20.736 15.104 34.752l113.088 53.248c12.288 5.76 20.16 18.048 20.16 31.616V492.8a34.88 34.88 0 0 1-69.824 0v-45.632l-106.048-49.472-13.504-6.4-4.8-14.08a269.632 269.632 0 0 0-15.68-38.72 338.88 338.88 0 0 0-25.664-37.952l-8.448-11.136 1.664-13.888 12.48-106.496-77.632-45.12-62.464 60.224-13.248 12.8-17.92-3.712c-26.432-5.376-46.016-8-57.856-8-11.648 0-30.336 2.56-55.168 7.936l-16.448 3.52-12.992-10.624-77.568-63.488-68.48 38.016 21.76 99.648 4.224 19.392-14.528 13.568c-17.792 16.64-30.016 29.952-36.288 39.168a185.728 185.728 0 0 0-19.2 41.152l-5.44 15.36-15.296 5.696-93.632 34.816v101.632l94.464 37.76 16.064 6.4 4.608 16.768c4.8 17.536 9.728 30.528 14.272 38.592 4.416 7.744 12.032 17.088 22.912 27.648l13.44 12.992-3.392 18.368-19.072 104.384 77.376 49.92c16 10.24 20.736 31.36 10.88 47.552l-0.448 0.64a34.88 34.88 0 0 1-48.256 10.432l-94.4-60.8a39.552 39.552 0 0 1-17.472-40.32l19.52-106.816-0.896-0.96a172.928 172.928 0 0 1-20.096-27.2L192 664.96c-5.696-10.048-10.88-22.4-15.616-37.056l-0.192-0.576-97.792-39.04a39.616 39.616 0 0 1-24.896-35.968V408.32c0-16.512 10.304-31.36 25.792-37.12l98.432-36.544c6.592-16.256 13.44-29.568 20.672-40.512l0.896-1.28c7.296-10.624 17.728-22.656 31.424-36.416l-21.952-103.04a39.552 39.552 0 0 1 18.688-42.56l0.768-0.384 106.048-58.88a39.552 39.552 0 0 1 44.288 3.904z m225.856 605.632l-50.048 121.792h98.688l-48.64-121.792z m-87.68-350.848c79.808 0 149.76 51.328 174.528 125.952a34.88 34.88 0 0 1-66.304 21.952 114.112 114.112 0 1 0-157.312 138.88 34.88 34.88 0 1 1-30.08 63.04 183.872 183.872 0 0 1 79.168-349.824z" p-id="6810"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1759963509092" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11021" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1013.775951 516.722584a12.8 12.8 0 0 1-9.322496 9.322495l-198.487702 49.599298a320 320 0 0 0-232.836121 232.836121l-49.644553 198.532957a12.8 12.8 0 0 1-24.844904-0.045255l-49.599298-198.487702a320 320 0 0 0-232.83612-232.836121L17.626545 525.95457a12.8 12.8 0 0 1 0-24.799649l198.532957-49.644553a320 320 0 0 0 232.836121-232.836121L498.640175 20.2318a12.8 12.8 0 0 1 24.844904-0.045255l49.644553 198.532957a320 320 0 0 0 232.836121 232.836121l198.487702 49.599298a12.8 12.8 0 0 1 9.322496 15.567663zM713.374363 513.554745l-9.956064-4.525483a409.792 409.792 0 0 1-187.807561-187.807561l-4.525483-9.956064-4.615993 9.956064a409.792 409.792 0 0 1-187.807561 187.807561l-9.956064 4.525483 10.001319 4.661248a409.792 409.792 0 0 1 187.762306 187.762306l4.570738 9.910809 4.615993-9.865554a409.792 409.792 0 0 1 187.807561-187.807561l9.910809-4.661248z" p-id="11022"></path><path d="M887.453079 38.673217l5.656855 22.763181c11.087434 44.576011 45.888402 79.376979 90.509668 90.509668l22.763181 5.656855a12.8 12.8 0 0 1-0.045255 24.844904l-22.717926 5.702109a124.416 124.416 0 0 0-90.509668 90.509668l-5.702109 22.717926a12.8 12.8 0 0 1-24.79965 0l-5.702109-22.717926a124.416 124.416 0 0 0-90.509668-90.509668l-22.717926-5.702109a12.8 12.8 0 0 1-0.045255-24.844904l22.763181-5.656855c44.576011-11.177944 79.376979-45.978911 90.509668-90.509668l5.656855-22.763181a12.8 12.8 0 0 1 24.890158 0z" p-id="11023"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1763871574824" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1900" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M618.666667 149.333333a42.666667 42.666667 0 0 0-42.666667-42.666666h-128a42.666667 42.666667 0 1 0 0 85.333333h21.333333v42.666667h-149.333333a170.666667 170.666667 0 0 0-170.666667 170.666666v320a170.666667 170.666667 0 0 0 170.666667 170.666667h384a170.666667 170.666667 0 0 0 170.666667-170.666667V405.333333a170.666667 170.666667 0 0 0-170.666667-170.666666h-149.333333V192h21.333333a42.666667 42.666667 0 0 0 42.666667-42.666667zM320 810.666667a85.333333 85.333333 0 0 1-85.333333-85.333334V405.333333a85.333333 85.333333 0 0 1 85.333333-85.333333h384a85.333333 85.333333 0 0 1 85.333333 85.333333v320a85.333333 85.333333 0 0 1-85.333333 85.333334H320zM64 448a42.666667 42.666667 0 0 0-42.666667 42.666667v149.333333a42.666667 42.666667 0 1 0 85.333334 0v-149.333333a42.666667 42.666667 0 0 0-42.666667-42.666667z m896 0a42.666667 42.666667 0 0 0-42.666667 42.666667v149.333333a42.666667 42.666667 0 1 0 85.333334 0v-149.333333a42.666667 42.666667 0 0 0-42.666667-42.666667z m-512 117.333333a53.333333 53.333333 0 1 0-106.666667 0 53.333333 53.333333 0 0 0 106.666667 0z m234.666667 0a53.333333 53.333333 0 1 0-106.666667 0 53.333333 53.333333 0 0 0 106.666667 0z" p-id="1901"></path></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -186,14 +186,14 @@ public class AgentQueryBehavior extends ANTLRAssistBehavior {
}
}
}
if (getSettingService().getAISetting().getLiteModelSetting() == null)
if (getSettingService().getAiSetting().getLiteModelSetting() == null)
hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to query with natural language</a>"));
return hints;
}
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
var liteModel = getSettingService().getAiSetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {

View File

@ -256,14 +256,14 @@ public class BuildQueryBehavior extends ANTLRAssistBehavior {
}
}
}
if (getSettingService().getAISetting().getLiteModelSetting() == null)
if (getSettingService().getAiSetting().getLiteModelSetting() == null)
hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to query with natural language"));
return hints;
}
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
var liteModel = getSettingService().getAiSetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {

View File

@ -159,7 +159,7 @@ public class CommitQueryBehavior extends ANTLRAssistBehavior {
}
}
}
if (getSettingService().getAISetting().getLiteModelSetting() == null)
if (getSettingService().getAiSetting().getLiteModelSetting() == null)
hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to query with natural language</a>"));
return hints;
}
@ -201,7 +201,7 @@ public class CommitQueryBehavior extends ANTLRAssistBehavior {
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
var liteModel = getSettingService().getAiSetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {

View File

@ -416,7 +416,7 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior {
}
}
}
if (getSettingService().getAISetting().getLiteModelSetting() == null)
if (getSettingService().getAiSetting().getLiteModelSetting() == null)
hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to query with natural language</a>"));
return hints;
}
@ -429,7 +429,7 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior {
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
var liteModel = getSettingService().getAiSetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {

View File

@ -226,14 +226,14 @@ public class PackQueryBehavior extends ANTLRAssistBehavior {
}
}
}
if (getSettingService().getAISetting().getLiteModelSetting() == null)
if (getSettingService().getAiSetting().getLiteModelSetting() == null)
hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to query with natural language</a>"));
return hints;
}
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
var liteModel = getSettingService().getAiSetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {

View File

@ -225,14 +225,14 @@ public class ProjectQueryBehavior extends ANTLRAssistBehavior {
}
}
}
if (getSettingService().getAISetting().getLiteModelSetting() == null)
if (getSettingService().getAiSetting().getLiteModelSetting() == null)
hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to query with natural language</a>"));
return hints;
}
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
var liteModel = getSettingService().getAiSetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {

View File

@ -261,7 +261,7 @@ public class PullRequestQueryBehavior extends ANTLRAssistBehavior {
}
}
}
if (getSettingService().getAISetting().getLiteModelSetting() == null)
if (getSettingService().getAiSetting().getLiteModelSetting() == null)
hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to query with natural language</a>"));
return hints;
}
@ -274,7 +274,7 @@ public class PullRequestQueryBehavior extends ANTLRAssistBehavior {
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
var liteModel = getSettingService().getAiSetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {

View File

@ -0,0 +1,28 @@
<wicket:panel>
<div class="ui-resizable-handle ui-resizable-w"></div>
<div class="head d-flex align-items-center justify-content-between px-4 py-3 border-bottom">
<h5 class="card-title mb-0"><wicket:t>Chat with</wicket:t> <a wicket:id="aiSelector" class="ml-2"><img wicket:id="avatar"></img> <span wicket:id="name"></span> <wicket:svg href="arrow" class="icon rotate-90"></wicket:svg></a></h5>
<a wicket:id="close" t:data-tippy-content="Close" class="btn btn-xs btn-icon btn-light btn-hover-primary"><wicket:svg href="times" class="icon"/></a>
</div>
<div class="body d-flex flex-column flex-grow-1 p-4 position-relative" style="overflow: hidden;">
<div wicket:id="chatSelectorContainer" class="chat-selector d-flex flex-shrink-0 mb-3">
<input wicket:id="chatSelector" t:placeholder="New chat" type="hidden" class="form-control">
<a wicket:id="newChat" t:data-tippy-content="New chat" class="btn btn-light btn-hover-primary btn-icon flex-shrink-0"><wicket:svg href="plus" class="icon icon-lg"></wicket:svg></a>
<a wicket:id="deleteChat" t:data-tippy-content="Delete chat" class="btn btn-light btn-hover-danger btn-icon flex-shrink-0"><wicket:svg href="trash" class="icon icon-lg"></wicket:svg></a>
</div>
<ul class="messages list-unstyled mb-2 flex-grow-1 overflow-auto">
<li wicket:id="messages" class="message">
<div wicket:id="content"></div>
</li>
<li wicket:id="responding" class="responding">
<div class="breathing-dot"></div>
<div wicket:id="content" class="mt-2"></div>
</li>
</ul>
<form wicket:id="send" class="send flex-shrink-0 position-relative">
<textarea wicket:id="input" class="form-control" t:placeholder="Type your message here"></textarea>
<a wicket:id="submit" class="submit position-absolute btn btn-primary btn-icon btn-sm" t:data-tippy-content="Send"><wicket:svg href="arrow2" class="icon rotate-270"></wicket:svg></a>
<a wicket:id="stop" class="stop position-absolute btn btn-primary btn-icon btn-sm" t:data-tippy-content="Stop"><wicket:svg href="stop" class="icon"></wicket:svg></a>
</form>
</div>
</wicket:panel>

View File

@ -0,0 +1,524 @@
package io.onedev.server.web.component.ai.chat;
import static io.onedev.server.security.SecurityUtils.getUser;
import static io.onedev.server.web.translation.Translation._T;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.servlet.http.Cookie;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.json.JSONException;
import org.json.JSONWriter;
import org.jspecify.annotations.Nullable;
import io.onedev.server.model.Chat;
import io.onedev.server.model.ChatMessage;
import io.onedev.server.model.User;
import io.onedev.server.persistence.dao.Dao;
import io.onedev.server.service.ChatService;
import io.onedev.server.service.UserService;
import io.onedev.server.service.support.ChatResponding;
import io.onedev.server.web.WebConstants;
import io.onedev.server.web.WebSession;
import io.onedev.server.web.behavior.ChangeObserver;
import io.onedev.server.web.behavior.OnTypingDoneBehavior;
import io.onedev.server.web.component.MultilineLabel;
import io.onedev.server.web.component.floating.FloatingPanel;
import io.onedev.server.web.component.markdown.MarkdownViewer;
import io.onedev.server.web.component.menu.MenuItem;
import io.onedev.server.web.component.menu.MenuLink;
import io.onedev.server.web.component.select2.ChoiceProvider;
import io.onedev.server.web.component.select2.ResponseFiller;
import io.onedev.server.web.component.select2.Select2Choice;
import io.onedev.server.web.component.user.UserAvatar;
public class ChatPanel extends Panel {
private static final long serialVersionUID = 1L;
private static final String COOKIE_ACTIVE_AI = "active-ai";
@Inject
private UserService userService;
@Inject
private ChatService chatService;
@Inject
private Dao dao;
private Long activeAiId;
private RepeatingView messagesView;
private WebMarkupContainer respondingContainer;
public ChatPanel(String componentId) {
super(componentId);
WebRequest request = (WebRequest) RequestCycle.get().getRequest();
Cookie cookie = request.getCookie(COOKIE_ACTIVE_AI);
if (cookie != null)
activeAiId = Long.valueOf(cookie.getValue());
}
@Override
protected void onInitialize() {
super.onInitialize();
var aiSelector = new MenuLink("aiSelector") {
@Override
protected List<MenuItem> getMenuItems(FloatingPanel dropdown) {
var activeAI = getActiveAI();
var menuItems = new ArrayList<MenuItem>();
for (var ai : getUser().getEntitledAis()) {
menuItems.add(new MenuItem() {
@Override
public String getLabel() {
return ai.getDisplayName();
}
@Override
public WebMarkupContainer newLink(String id) {
return new AjaxLink<Void>(id) {
@Override
public void onClick(AjaxRequestTarget target) {
setActiveAI(ai);
WebSession.get().setActiveChatId(null);
target.add(ChatPanel.this);
}
};
}
public boolean isSelected() {
return ai.equals(activeAI);
}
});
}
return menuItems;
}
@Override
protected void onBeforeRender() {
addOrReplace(new UserAvatar("avatar", getActiveAI()));
addOrReplace(new Label("name", getActiveAI().getDisplayName()));
super.onBeforeRender();
}
};
add(aiSelector);
add(new AjaxLink<Void>("close") {
@Override
public void onClick(AjaxRequestTarget target) {
hide(target);
}
});
var chatSelectorContainer = new WebMarkupContainer("chatSelectorContainer");
chatSelectorContainer.setOutputMarkupId(true);
chatSelectorContainer.add(new Select2Choice<Chat>("chatSelector", new IModel<Chat>() {
@Override
public void detach() {
}
@Override
public Chat getObject() {
return getActiveChat();
}
@Override
public void setObject(Chat object) {
WebSession.get().setActiveChatId(object.getId());
}
}, new ChoiceProvider<Chat>() {
@Override
public void query(String term, int page, io.onedev.server.web.component.select2.Response<Chat> response) {
var count = (page+1) * WebConstants.PAGE_SIZE + 1;
var chats = chatService.query(getUser(), getActiveAI(), term, count);
new ResponseFiller<>(response).fill(chats, page, WebConstants.PAGE_SIZE);
}
@Override
public void toJson(Chat choice, JSONWriter writer) throws JSONException {
writer.key("id").value(choice.getId()).key("text").value(choice.getTitle());
}
@Override
public Collection<Chat> toChoices(Collection<String> ids) {
return ids.stream().map(it->chatService.load(Long.valueOf(it))).collect(Collectors.toList());
}
}).add(new AjaxFormComponentUpdatingBehavior("change") {
@Override
protected void onUpdate(AjaxRequestTarget target) {
target.add(ChatPanel.this);
}
}));
chatSelectorContainer.add(new ChangeObserver() {
@Override
protected Collection<String> findObservables() {
var chat = getActiveChat();
if (chat != null)
return Collections.singleton(Chat.getChangeObservable(chat.getId()));
else
return Collections.emptySet();
}
public void onObservableChanged(IPartialPageRequestHandler handler, Collection<String> changedObservables) {
handler.add(chatSelectorContainer);
}
});
chatSelectorContainer.add(new ChangeObserver() {
@Override
protected Collection<String> findObservables() {
var chat = getActiveChat();
if (chat != null)
return Collections.singleton(Chat.getNewMessagesObservable(chat.getId()));
else
return Collections.emptySet();
}
@Override
public void onObservableChanged(IPartialPageRequestHandler handler, Collection<String> changedObservables) {
showNewMessages(handler);
}
});
chatSelectorContainer.add(new AjaxLink<Void>("newChat") {
@Override
public void onClick(AjaxRequestTarget target) {
WebSession.get().setActiveChatId(null);
target.add(ChatPanel.this);
}
});
chatSelectorContainer.add(new AjaxLink<Void>("deleteChat") {
@Override
public void onClick(AjaxRequestTarget target) {
getSession().success(_T("Chat deleted"));
chatService.delete(getActiveChat());
WebSession.get().setActiveChatId(null);
target.add(ChatPanel.this);
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(getActiveChat() != null);
}
});
add(chatSelectorContainer);
respondingContainer = new WebMarkupContainer("responding") {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(getResponding() != null);
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
response.render(OnDomReadyHeaderItem.forScript(String.format("""
setTimeout(() => {
var $responding = $('#%s');
if ($responding.is(":visible"))
$responding[0].scrollIntoView({ block: "end" });
}, 0);
""", getMarkupId())));
}
};
respondingContainer.add(new MarkdownViewer("content", new AbstractReadOnlyModel<String>() {
@Override
public String getObject() {
var responding = getResponding();
if (responding != null)
return responding.getContent();
else
return null;
}
}, null));
respondingContainer.add(new ChangeObserver() {
@Override
protected Collection<String> findObservables() {
var chat = getActiveChat();
if (chat != null)
return Collections.singleton(Chat.getPartialResponseObservable(chat.getId()));
else
return Collections.emptySet();
}
});
respondingContainer.setOutputMarkupPlaceholderTag(true);
add(respondingContainer);
var form = new Form<Void>("send");
form.add(new TextArea<String>("input", new IModel<String>() {
@Override
public void detach() {
}
@Override
public String getObject() {
return WebSession.get().getChatInput();
}
@Override
public void setObject(String object) {
WebSession.get().setChatInput(object);
}
}).add(new OnTypingDoneBehavior() {
@Override
protected void onTypingDone(AjaxRequestTarget target) {
}
}));
form.setOutputMarkupId(true);
form.add(new AjaxButton("submit") {
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
var input = WebSession.get().getChatInput().trim();
var chat = getActiveChat();
if (chat == null) {
chat = new Chat();
chat.setUser(getUser());
chat.setAi(getActiveAI());
chat.setTitle(_T("New chat"));
chat.setDate(new Date());
chatService.createOrUpdate(chat);
WebSession.get().setActiveChatId(chat.getId());
target.add(chatSelectorContainer);
}
var request = new ChatMessage();
request.setChat(chat);
request.setRequest(true);
request.setContent(input);
chat.getMessages().add(request);
dao.persist(request);
chatService.sendRequest(getSession().getId(), request);
showNewMessages(target);
target.add(respondingContainer);
WebSession.get().setChatInput(null);
target.appendJavaScript("""
var $send = $(".chat>.body>.send");
$send.find("textarea").val("");
$send.find("a.submit").attr("disabled", "disabled");
""");
}
});
form.add(new AjaxLink<Void>("stop") {
@Override
public void onClick(AjaxRequestTarget target) {
var responding = getResponding();
if (responding != null)
responding.cancel();
}
});
add(form);
add(AttributeAppender.append("class", "chat d-flex flex-column"));
setOutputMarkupPlaceholderTag(true);
}
private User getActiveAI() {
if (activeAiId != null) {
var ai = userService.get(activeAiId);
if (ai != null && getUser().getEntitledAis().contains(ai))
return ai;
}
return getUser().getEntitledAis().get(0);
}
private void setActiveAI(User ai) {
activeAiId = ai.getId();
WebResponse response = (WebResponse) RequestCycle.get().getResponse();
Cookie cookie = new Cookie(COOKIE_ACTIVE_AI, activeAiId.toString());
cookie.setMaxAge(Integer.MAX_VALUE);
cookie.setPath("/");
response.addCookie(cookie);
}
@Nullable
private Chat getActiveChat() {
var activeChatId = WebSession.get().getActiveChatId();
if (activeChatId != null) {
var chat = chatService.get(activeChatId);
if (chat != null && chat.getAi().equals(getActiveAI()))
return chat;
}
return null;
}
@Nullable
private ChatResponding getResponding() {
var chat = getActiveChat();
if (chat != null)
return chatService.getResponding(getSession().getId(), chat);
else
return null;
}
private List<ChatMessage> getMessages() {
var chat = getActiveChat();
if (chat != null)
return chat.getSortedMessages();
else
return Collections.emptyList();
}
@SuppressWarnings("deprecation")
private void showNewMessages(IPartialPageRequestHandler handler) {
long lastMessageId;
if (messagesView.size() != 0)
lastMessageId = (Long) messagesView.get(messagesView.size() - 1).getDefaultModelObject();
else
lastMessageId = 0;
getMessages().stream().filter(it -> it.getId() > lastMessageId).forEach(it -> {
var messageContainer = newMessageContainer(messagesView.newChildId(), it);
messagesView.add(messageContainer);
handler.prependJavaScript(String.format("""
$('#%s').before($("<li class='message' id='%s'></li>"));
""", respondingContainer.getMarkupId(), messageContainer.getMarkupId()));
handler.add(messageContainer);
});
var lastMessage = messagesView.get(messagesView.size() - 1);
handler.appendJavaScript(String.format("""
$('#%s')[0].scrollIntoView({ block: "end" });
""", lastMessage.getMarkupId()));
}
private Component newMessageContainer(String containerId, ChatMessage message) {
var messageContainer = new WebMarkupContainer(containerId, Model.of(message.getId()));
if (message.isError() || message.isRequest()) {
messageContainer.add(new MultilineLabel("content", message.getContent()));
} else {
messageContainer.add(new MarkdownViewer("content", Model.of(message.getContent()), null));
}
if (message.isError())
messageContainer.add(AttributeAppender.append("class", "error"));
else if (message.isRequest())
messageContainer.add(AttributeAppender.append("class", "request"));
else
messageContainer.add(AttributeAppender.append("class", "response"));
messageContainer.setOutputMarkupId(true);
return messageContainer;
}
@Override
protected void onBeforeRender() {
messagesView = new RepeatingView("messages");
for (var message: getMessages()) {
messagesView.add(newMessageContainer(messagesView.newChildId(), message));
}
addOrReplace(messagesView);
WebRequest request = (WebRequest) RequestCycle.get().getRequest();
Cookie cookie = request.getCookie("chat.width");
if (cookie != null)
add(AttributeAppender.replace("style", "width:" + cookie.getValue() + "px"));
else
add(AttributeAppender.replace("style", "width:400px"));
super.onBeforeRender();
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(WebSession.get().isChatVisible());
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
response.render(JavaScriptHeaderItem.forReference(new ChatResourceReference()));
response.render(OnDomReadyHeaderItem.forScript("onedev.server.chat.onDomReady();"));
}
@Override
public boolean isVisible() {
return WebSession.get().isChatVisible();
}
public void show(AjaxRequestTarget target) {
if (!isVisible()) {
WebSession.get().setChatVisible(true);
WebSession.get().setActiveChatId(null);
target.add(this);
}
}
public void hide(AjaxRequestTarget target) {
WebSession.get().setChatVisible(false);
target.add(this);
}
}

View File

@ -0,0 +1,29 @@
package io.onedev.server.web.component.ai.chat;
import java.util.List;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.HeaderItem;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import io.onedev.server.web.asset.jqueryui.JQueryUIResourceReference;
import io.onedev.server.web.page.base.BaseDependentCssResourceReference;
import io.onedev.server.web.page.base.BaseDependentResourceReference;
public class ChatResourceReference extends BaseDependentResourceReference {
private static final long serialVersionUID = 1L;
public ChatResourceReference() {
super(ChatResourceReference.class, "chat.js");
}
@Override
public List<HeaderItem> getDependencies() {
List<HeaderItem> dependencies = super.getDependencies();
dependencies.add(JavaScriptHeaderItem.forReference(new JQueryUIResourceReference()));
dependencies.add(CssHeaderItem.forReference(new BaseDependentCssResourceReference(getScope(), "chat.css")));
return dependencies;
}
}

View File

@ -0,0 +1,133 @@
.chat {
position: fixed;
top: calc(var(--topbar-height) + 1rem);
bottom: 0;
right: 0;
z-index: 1000;
border-radius: 0.42rem 0 0 0;
background: white;
box-shadow: 0 0 12px rgba(0,0,0,0.2), 0 -2px 8px rgba(0,0,0,0.1);
}
.chat>.head {
border-radius: 0.42rem 0 0 0;
}
.dark-mode .chat {
background-color: var(--dark-mode-dark);
box-shadow: 0 0 20px rgb(0 0 0 / 50%), 0 -2px 15px rgb(0 0 0 / 30%);
}
.chat .ui-resizable-handle {
position: absolute;
left: 0;
top: 0.42rem;
bottom: 0;
width: 3px;
cursor: col-resize;
z-index: 10;
background: var(--light) url(/~icon/grip3.svg) no-repeat scroll center center;
background-size: 18px 18px;
}
.dark-mode .chat .ui-resizable-handle {
background: var(--dark-mode-light-dark) url(/~icon/dark-grip3.svg) no-repeat scroll center center;
background-size: 18px 18px;
}
.chat>.body>.chat-selector {
overflow: hidden;
min-width: 0;
gap: 0.5rem;
}
.chat>.body>.chat-selector>:first-child {
flex: 1;
min-width: 0;
overflow: hidden;
}
.chat>.body>.send textarea {
max-height: 160px;
}
.chat>.body>.send a {
border-radius: 50px;
width: 27px;
height: 27px;
right: 6px;
bottom: 6px;
}
.chat>.body>.send a.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.chat>.body>.messages>li {
margin-bottom: 1rem;
}
.chat>.body>.messages>li:last-child {
margin-bottom: 0;
}
.chat>.body>.messages>.request {
display: flex;
justify-content: flex-end;
}
.chat>.body>.messages>.response+.responding {
display: none;
}
.chat>.body>.messages>.response, .chat>.body>.messages>.error {
display: flex;
justify-content: flex-start;
}
.chat>.body>.messages>.response>div, .chat>.body>.messages>.error>div {
margin-right: 3rem;
}
.chat>.body>.messages>.request>div {
background: var(--light);
padding: 0.7rem;
border-radius: 0.42rem;
margin-left: 3rem;
}
.dark-mode .chat>.body>.messages>.request>div {
background: var(--dark-mode-light-dark);
}
.chat>.body>.messages>.error>div {
background: var(--light-danger);
padding: 0.7rem;
border-radius: 0.42rem;
}
.dark-mode .chat>.body>.messages>.error>div {
background: #3a2434;
color: var(--danger);
}
.chat>.body>.messages>.responding .breathing-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: var(--gray-dark);
animation: breathing 1.5s ease-in-out infinite;
}
.dark-mode .chat>.body>.messages>.responding .breathing-dot {
background-color: var(--dark-mode-gray);
}
@keyframes breathing {
0%, 100% {
transform: scale(0.6);
}
50% {
transform: scale(1);
}
}
.chat>.body:has(.messages>.responding:not(.response + .responding))>.send .submit {
display: none;
}
.chat>.body:not(:has(.messages>.responding:not(.response + .responding)))>.send .stop {
display: none;
}
.chat>.body>.send .stop svg {
width: 12px !important;
height: 12px !important;
}

View File

@ -0,0 +1,41 @@
onedev.server.chat = {
onDomReady: function() {
var $chat = $(".chat");
var $resizeHandle = $chat.children(".ui-resizable-handle");
$chat.resizable({
autoHide: false,
handles: {"w": $resizeHandle},
minWidth: 300,
stop: function(e, ui) {
Cookies.set("chat.width", ui.size.width, {expires: Infinity});
}
});
var $textarea = $chat.find(">.body>.send textarea");
var $submit = $chat.find(">.body>.send a.submit");
function updateSubmit() {
var $responding = $chat.find(">.body>.messages>.responding");
var isEmpty = $textarea.val().trim() === "";
$submit.toggleClass("disabled", isEmpty);
if (isEmpty && !$responding.is(":visible")) {
$submit.attr("disabled", "disabled");
} else {
$submit.removeAttr("disabled");
}
}
updateSubmit();
$textarea.on("input", updateSubmit);
$textarea.keydown(function(e) {
if (e.keyCode == 13 && !e.shiftKey) {
e.preventDefault();
if ($textarea.val().trim() !== "" && !$submit.hasClass("disabled")) {
$submit.click();
}
}
});
}
}

View File

@ -1373,6 +1373,19 @@ public abstract class RevisionDiffPanel extends Panel {
private WebMarkupContainer newNavigationContainer() {
WebMarkupContainer navigationContainer = new WebMarkupContainer("navigation") {
@Override
protected void onBeforeRender() {
WebRequest request = (WebRequest) RequestCycle.get().getRequest();
Cookie cookie = request.getCookie(COOKIE_NAVIGATION_WIDTH);
// cookie will not be sent for websocket request
if (cookie != null)
add(AttributeAppender.replace("style", "width:" + cookie.getValue() + "px"));
else
add(AttributeAppender.replace("style", "width:360px"));
super.onBeforeRender();
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
@ -1380,15 +1393,6 @@ public abstract class RevisionDiffPanel extends Panel {
}
};
float navigationWidth = 360;
WebRequest request = (WebRequest) RequestCycle.get().getRequest();
Cookie cookie = request.getCookie(COOKIE_NAVIGATION_WIDTH);
// cookie will not be sent for websocket request
if (cookie != null)
navigationWidth = Float.parseFloat(cookie.getValue());
navigationContainer.add(AttributeAppender.append("style", "width:" + navigationWidth + "px"));
var changes = new TreeMap<String, BlobChange>();
var treeState = new HashSet<String>();
@ -1551,6 +1555,19 @@ public abstract class RevisionDiffPanel extends Panel {
private WebMarkupContainer newCommentContainer() {
WebMarkupContainer commentContainer = new WebMarkupContainer("comment", Model.of((Mark)null)) {
@Override
protected void onBeforeRender() {
WebRequest request = (WebRequest) RequestCycle.get().getRequest();
Cookie cookie = request.getCookie(COOKIE_COMMENT_WIDTH);
// cookie will not be sent for websocket request
if (cookie != null)
add(AttributeAppender.replace("style", "width:" + cookie.getValue() + "px"));
else
add(AttributeAppender.replace("style", "width:360px"));
super.onBeforeRender();
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
@ -1560,15 +1577,6 @@ public abstract class RevisionDiffPanel extends Panel {
};
commentContainer.setOutputMarkupPlaceholderTag(true);
float commentWidth = 360;
WebRequest request = (WebRequest) RequestCycle.get().getRequest();
Cookie cookie = request.getCookie(COOKIE_COMMENT_WIDTH);
// cookie will not be sent for websocket request
if (cookie != null)
commentWidth = Float.parseFloat(cookie.getValue());
commentContainer.add(AttributeAppender.append("style", "width:" + commentWidth + "px"));
WebMarkupContainer head = new WebMarkupContainer("head");
head.setOutputMarkupId(true);
commentContainer.add(head);

View File

@ -1,5 +1,6 @@
package io.onedev.server.web.component.entity.watches;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.web.translation.Translation._T;
import java.util.ArrayList;
@ -109,7 +110,7 @@ public abstract class EntityWatchesPanel extends Panel {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(SecurityUtils.getAuthUser() != null && !SecurityUtils.getAuthUser().isServiceAccount());
setVisible(SecurityUtils.getAuthUser() != null && SecurityUtils.getAuthUser().getType() == ORDINARY);
}
@Override

View File

@ -1,5 +1,6 @@
package io.onedev.server.web.component.issue.list;
import static io.onedev.server.model.User.Type.SERVICE;
import static io.onedev.server.web.component.issue.list.BuiltInFieldsBean.NAME_CONFIDENTIAL;
import static io.onedev.server.web.component.issue.list.BuiltInFieldsBean.NAME_ITERATION;
import static io.onedev.server.web.component.issue.list.BuiltInFieldsBean.NAME_STATE;
@ -20,7 +21,6 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import javax.validation.ValidationException;
import org.apache.wicket.AttributeModifier;
@ -39,14 +39,13 @@ import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.PropertyModel;
import org.jspecify.annotations.Nullable;
import com.google.common.collect.Lists;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.InputContext;
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
import io.onedev.server.service.IssueChangeService;
import io.onedev.server.service.SettingService;
import io.onedev.server.model.Issue;
import io.onedev.server.model.Iteration;
import io.onedev.server.model.Project;
@ -55,6 +54,8 @@ import io.onedev.server.model.support.issue.field.FieldUtils;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
import io.onedev.server.search.entity.issue.IssueQuery;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.service.IssueChangeService;
import io.onedev.server.service.SettingService;
import io.onedev.server.web.ajaxlistener.DisableGlobalAjaxIndicatorListener;
import io.onedev.server.web.behavior.RunTaskBehavior;
import io.onedev.server.web.component.comment.CommentInput;
@ -267,7 +268,7 @@ abstract class BatchEditPanel extends Panel implements InputContext {
});
form.add(new CheckBox("sendNotifications", new PropertyModel<>(this, "sendNotifications"))
.setVisible(!SecurityUtils.getUser().isServiceAccount()));
.setVisible(SecurityUtils.getUser().getType() != SERVICE));
form.add(new AjaxButton("save") {

View File

@ -1,6 +1,7 @@
package io.onedev.server.web.component.issue.list;
import static com.google.common.collect.Lists.newArrayList;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.search.entity.EntitySort.Direction.ASCENDING;
import static io.onedev.server.search.entity.issue.IssueQuery.merge;
import static io.onedev.server.web.translation.Translation._T;
@ -20,8 +21,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.Predicate;
import org.apache.commons.csv.CSVFormat;
@ -70,6 +69,7 @@ import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.AbstractResource;
import org.dhatim.fastexcel.Workbook;
import org.jspecify.annotations.Nullable;
import com.google.common.collect.Sets;
@ -78,12 +78,6 @@ import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.service.AuditService;
import io.onedev.server.service.IssueLinkService;
import io.onedev.server.service.IssueService;
import io.onedev.server.service.IssueWatchService;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.SettingService;
import io.onedev.server.imports.IssueImporter;
import io.onedev.server.imports.IssueImporterContribution;
import io.onedev.server.model.Issue;
@ -105,6 +99,12 @@ import io.onedev.server.search.entity.issue.IssueQuery;
import io.onedev.server.search.entity.issue.IssueQueryParseOption;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.security.permission.AccessProject;
import io.onedev.server.service.AuditService;
import io.onedev.server.service.IssueLinkService;
import io.onedev.server.service.IssueService;
import io.onedev.server.service.IssueWatchService;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.SettingService;
import io.onedev.server.timetracking.TimeTrackingService;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.LinkDescriptor;
@ -1575,7 +1575,7 @@ public abstract class IssueListPanel extends Panel {
});
}
if (!SecurityUtils.getAuthUser().isServiceAccount()) {
if (SecurityUtils.getAuthUser().getType() == ORDINARY) {
menuItems.add(new MenuItem() {
@Override

View File

@ -1,27 +1,11 @@
package io.onedev.server.web.component.markdown;
import io.onedev.commons.loader.AppLoader;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.service.*;
import io.onedev.server.entityreference.BuildReference;
import io.onedev.server.entityreference.EntityReference;
import io.onedev.server.entityreference.IssueReference;
import io.onedev.server.entityreference.PullRequestReference;
import io.onedev.server.markdown.MarkdownService;
import io.onedev.server.model.Project;
import io.onedev.server.model.User;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.ColorUtils;
import io.onedev.server.util.DateUtils;
import io.onedev.server.web.asset.emoji.Emojis;
import io.onedev.server.web.asset.lozad.LozadResourceReference;
import io.onedev.server.web.avatar.AvatarService;
import io.onedev.server.web.behavior.AbstractPostAjaxBehavior;
import io.onedev.server.web.component.build.status.BuildStatusIcon;
import io.onedev.server.web.component.svg.SpriteImage;
import io.onedev.server.web.page.project.ProjectPage;
import io.onedev.server.web.page.project.blob.render.BlobRenderContext;
import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;
import java.util.List;
import javax.inject.Inject;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.ComponentTag;
@ -38,13 +22,35 @@ import org.apache.wicket.request.cycle.RequestCycle;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.hibernate.StaleStateException;
import org.jspecify.annotations.Nullable;
import org.unbescape.html.HtmlEscape;
import org.unbescape.javascript.JavaScriptEscape;
import org.jspecify.annotations.Nullable;
import java.util.List;
import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.entityreference.BuildReference;
import io.onedev.server.entityreference.EntityReference;
import io.onedev.server.entityreference.IssueReference;
import io.onedev.server.entityreference.PullRequestReference;
import io.onedev.server.markdown.MarkdownService;
import io.onedev.server.model.Project;
import io.onedev.server.model.User;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.service.BuildService;
import io.onedev.server.service.IssueService;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.PullRequestService;
import io.onedev.server.service.SettingService;
import io.onedev.server.service.UserService;
import io.onedev.server.util.ColorUtils;
import io.onedev.server.util.DateUtils;
import io.onedev.server.web.asset.emoji.Emojis;
import io.onedev.server.web.asset.lozad.LozadResourceReference;
import io.onedev.server.web.avatar.AvatarService;
import io.onedev.server.web.behavior.AbstractPostAjaxBehavior;
import io.onedev.server.web.component.build.status.BuildStatusIcon;
import io.onedev.server.web.component.svg.SpriteImage;
import io.onedev.server.web.page.project.ProjectPage;
import io.onedev.server.web.page.project.blob.render.BlobRenderContext;
public class MarkdownViewer extends GenericPanel<String> {
@ -67,6 +73,30 @@ public class MarkdownViewer extends GenericPanel<String> {
private AbstractPostAjaxBehavior referenceBehavior;
private AbstractPostAjaxBehavior suggestionBehavior;
@Inject
private MarkdownService markdownService;
@Inject
private IssueService issueService;
@Inject
private PullRequestService pullRequestService;
@Inject
private SettingService settingService;
@Inject
private BuildService buildService;
@Inject
private AvatarService avatarService;
@Inject
private UserService userService;
@Inject
private ProjectService projectService;
private final IModel<String> renderedModel = new LoadableDetachableModel<String>() {
@ -74,8 +104,7 @@ public class MarkdownViewer extends GenericPanel<String> {
protected String load() {
String markdown = getModelObject();
if (markdown != null) {
MarkdownService manager = AppLoader.getInstance(MarkdownService.class);
return manager.process(manager.render(markdown), getProject(),
return markdownService.process(markdownService.render(markdown), getProject(),
getRenderContext(), getSuggestionSupport(), false);
} else {
return null;
@ -183,10 +212,10 @@ public class MarkdownViewer extends GenericPanel<String> {
switch (referenceType) {
case "issue":
EntityReference reference = IssueReference.of(referenceId, null);
var issue = OneDev.getInstance(IssueService.class).find(reference.getProject(), reference.getNumber());
var issue = issueService.find(reference.getProject(), reference.getNumber());
// check permission here as issue project may not be the same as current project
if (issue != null && SecurityUtils.canAccessIssue(issue)) {
String color = OneDev.getInstance(SettingService.class).getIssueSetting().getStateSpec(issue.getState()).getColor();
String color = settingService.getIssueSetting().getStateSpec(issue.getState()).getColor();
String script = String.format("onedev.server.markdown.renderIssueTooltip('%s', '%s', '%s', '%s');",
Emojis.getInstance().apply(JavaScriptEscape.escapeJavaScript(issue.getTitle())),
JavaScriptEscape.escapeJavaScript(issue.getState()),
@ -198,7 +227,7 @@ public class MarkdownViewer extends GenericPanel<String> {
break;
case "pull request":
reference = PullRequestReference.of(referenceId, null);
var request = OneDev.getInstance(PullRequestService.class).find(reference.getProject(), reference.getNumber());
var request = pullRequestService.find(reference.getProject(), reference.getNumber());
// check permission here as target project may not be the same as current project
if (request != null && SecurityUtils.canReadCode(request.getTargetProject())) {
String status = request.getStatus().toString();
@ -224,7 +253,7 @@ public class MarkdownViewer extends GenericPanel<String> {
break;
case "build":
reference = BuildReference.of(referenceId, null);
var build = OneDev.getInstance(BuildService.class).find(reference.getProject(), reference.getNumber());
var build = buildService.find(reference.getProject(), reference.getNumber());
// check permission here as build project may not be the same as current project
if (build != null && SecurityUtils.canAccessBuild(build)) {
String iconHref = SpriteImage.getVersionedHref(BuildStatusIcon.getIconHref(build.getStatus()));
@ -241,9 +270,9 @@ public class MarkdownViewer extends GenericPanel<String> {
}
break;
case "user":
User user = OneDev.getInstance(UserService.class).findByName(referenceId);
User user = userService.findByName(referenceId);
if (user != null) {
String avatarUrl = OneDev.getInstance(AvatarService.class).getUserAvatarUrl(user.getId());
String avatarUrl = avatarService.getUserAvatarUrl(user.getId());
String script = String.format("onedev.server.markdown.renderUserTooltip('%s', '%s')",
JavaScriptEscape.escapeJavaScript(avatarUrl),
JavaScriptEscape.escapeJavaScript(user.getDisplayName()));
@ -254,8 +283,7 @@ public class MarkdownViewer extends GenericPanel<String> {
Project commitProject = getProject();
String commitHash;
if (referenceId.contains(":")) {
commitProject = OneDev.getInstance(ProjectService.class)
.findByPath(StringUtils.substringBefore(referenceId, ":"));
commitProject = projectService.findByPath(StringUtils.substringBefore(referenceId, ":"));
commitHash = StringUtils.substringAfter(referenceId, ":");
} else {
commitHash = referenceId;

View File

@ -1,6 +1,7 @@
package io.onedev.server.web.component.pullrequest.list;
import static io.onedev.server.entityreference.ReferenceUtils.transformReferences;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.search.entity.pullrequest.PullRequestQuery.merge;
import static io.onedev.server.web.translation.Translation._T;
import static java.util.stream.Collectors.toList;
@ -13,8 +14,6 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.Component;
import org.apache.wicket.Session;
@ -52,17 +51,13 @@ import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.cycle.RequestCycle;
import org.jspecify.annotations.Nullable;
import com.google.common.collect.Sets;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.OneDev;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.service.AuditService;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.PullRequestService;
import io.onedev.server.service.PullRequestReviewService;
import io.onedev.server.service.PullRequestWatchService;
import io.onedev.server.entityreference.LinkTransformer;
import io.onedev.server.model.Project;
import io.onedev.server.model.PullRequest;
@ -78,6 +73,11 @@ import io.onedev.server.search.entity.pullrequest.FuzzyCriteria;
import io.onedev.server.search.entity.pullrequest.PullRequestQuery;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.security.permission.ReadCode;
import io.onedev.server.service.AuditService;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.PullRequestReviewService;
import io.onedev.server.service.PullRequestService;
import io.onedev.server.service.PullRequestWatchService;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.watch.WatchStatus;
import io.onedev.server.web.WebConstants;
@ -271,7 +271,7 @@ public abstract class PullRequestListPanel extends Panel {
protected List<MenuItem> getMenuItems(FloatingPanel dropdown) {
List<MenuItem> menuItems = new ArrayList<>();
if (!SecurityUtils.getAuthUser().isServiceAccount()) {
if (SecurityUtils.getAuthUser().getType() == ORDINARY) {
menuItems.add(new MenuItem() {
@Override
@ -474,7 +474,7 @@ public abstract class PullRequestListPanel extends Panel {
});
}
if (!SecurityUtils.getAuthUser().isServiceAccount()) {
if (SecurityUtils.getAuthUser().getType() == ORDINARY) {
menuItems.add(new MenuItem() {
@Override

View File

@ -1,11 +1,12 @@
package io.onedev.server.web.component.savedquery;
import static io.onedev.server.model.User.Type.ORDINARY;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import javax.servlet.http.Cookie;
import org.apache.wicket.Component;
@ -31,6 +32,7 @@ import org.apache.wicket.model.Model;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.jspecify.annotations.Nullable;
import io.onedev.server.model.Project;
import io.onedev.server.model.support.NamedQuery;
@ -310,7 +312,7 @@ public abstract class SavedQueriesPanel<T extends NamedQuery> extends Panel {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(!SecurityUtils.getAuthUser().isServiceAccount() && getQueryPersonalization().getQueryWatchSupport() != null);
setVisible(SecurityUtils.getAuthUser().getType() == ORDINARY && getQueryPersonalization().getQueryWatchSupport() != null);
}
});
@ -336,7 +338,7 @@ public abstract class SavedQueriesPanel<T extends NamedQuery> extends Panel {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(!SecurityUtils.getAuthUser().isServiceAccount() && getQueryPersonalization().getQuerySubscriptionSupport() != null);
setVisible(SecurityUtils.getAuthUser().getType() == ORDINARY && getQueryPersonalization().getQuerySubscriptionSupport() != null);
}
});
@ -392,7 +394,7 @@ public abstract class SavedQueriesPanel<T extends NamedQuery> extends Panel {
protected void onConfigure() {
super.onConfigure();
setVisible(SecurityUtils.getAuthUser() != null
&& !SecurityUtils.getAuthUser().isServiceAccount()
&& SecurityUtils.getAuthUser().getType() == ORDINARY
&& getQueryPersonalization() != null
&& getQueryPersonalization().getQueryWatchSupport() != null);
}
@ -422,7 +424,7 @@ public abstract class SavedQueriesPanel<T extends NamedQuery> extends Panel {
protected void onConfigure() {
super.onConfigure();
setVisible(SecurityUtils.getAuthUser() != null
&& !SecurityUtils.getAuthUser().isServiceAccount()
&& SecurityUtils.getAuthUser().getType() == ORDINARY
&& getQueryPersonalization() != null
&& getQueryPersonalization().getQuerySubscriptionSupport() != null);
}

View File

@ -156,7 +156,7 @@ public abstract class SymbolTooltipPanel extends Panel {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(settingService.getAISetting().getLiteModelSetting() == null && symbolHits.size() > 1);
setVisible(settingService.getAiSetting().getLiteModelSetting() == null && symbolHits.size() > 1);
}
});
@ -311,7 +311,7 @@ public abstract class SymbolTooltipPanel extends Panel {
target.add(content);
CharSequence callback;
if (settingService.getAISetting().getLiteModelSetting() != null && symbolHits.size() > 1)
if (settingService.getAiSetting().getLiteModelSetting() != null && symbolHits.size() > 1)
callback = getCallbackFunction(explicit("action"));
else
callback = "undefined";
@ -326,7 +326,7 @@ public abstract class SymbolTooltipPanel extends Panel {
}
target.appendJavaScript(script);
} else {
var liteModel = settingService.getAISetting().getLiteModel();
var liteModel = settingService.getAiSetting().getLiteModel();
int index;
try {
ObjectMapper mapperCopy = objectMapper.copy();

View File

@ -0,0 +1,82 @@
package io.onedev.server.web.component.user.aisetting;
import static io.onedev.server.model.User.Type.ORDINARY;
import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;
import io.onedev.server.OneDev;
import io.onedev.server.annotation.DependsOn;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.GroupChoice;
import io.onedev.server.annotation.ProjectChoice;
import io.onedev.server.annotation.UserChoice;
import io.onedev.server.model.User;
import io.onedev.server.service.UserService;
@Editable
public class EntitlementEditBean implements Serializable{
private static final long serialVersionUID = 1L;
private boolean entitleToAll;
private List<String> entitledProjects;
private List<String> entitledGroups;
private List<String> entitledUsers;
@Editable(order=100, name="Entitle to All Users")
public boolean isEntitleToAll() {
return entitleToAll;
}
public void setEntitleToAll(boolean entitleToAll) {
this.entitleToAll = entitleToAll;
}
@Editable(order=200)
@ProjectChoice
@DependsOn(property="entitleToAll", value="false")
public List<String> getEntitledProjects() {
return entitledProjects;
}
public void setEntitledProjects(List<String> entitledProjects) {
this.entitledProjects = entitledProjects;
}
@Editable(order=300)
@GroupChoice
@DependsOn(property="entitleToAll", value="false")
public List<String> getEntitledGroups() {
return entitledGroups;
}
public void setEntitledGroups(List<String> entitledGroups) {
this.entitledGroups = entitledGroups;
}
@Editable(order=300)
@UserChoice("getUsers")
@DependsOn(property="entitleToAll", value="false")
public List<String> getEntitledUsers() {
return entitledUsers;
}
public void setEntitledUsers(List<String> entitledUsers) {
this.entitledUsers = entitledUsers;
}
@SuppressWarnings("unused")
private static List<User> getUsers() {
var cache = OneDev.getInstance(UserService.class).cloneCache();
return cache.getUsers(it->!it.isDisabled() && it.getType() == ORDINARY)
.stream()
.sorted(cache.comparingDisplayName())
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,9 @@
<wicket:panel>
<div class="alert alert-light">
<wicket:t>Specifies who can access this AI service</wicket:t>
</div>
<form wicket:id="form" class="leave-confirm">
<div wicket:id="editor" class="mb-4"></div>
<input type="submit" class="btn btn-primary dirty-aware" t:value="Save Settings">
</form>
</wicket:panel>

View File

@ -0,0 +1,132 @@
package io.onedev.server.web.component.user.aisetting;
import static io.onedev.server.web.translation.Translation._T;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.panel.GenericPanel;
import org.apache.wicket.model.IModel;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.model.GroupEntitlement;
import io.onedev.server.model.ProjectEntitlement;
import io.onedev.server.model.User;
import io.onedev.server.model.UserEntitlement;
import io.onedev.server.persistence.TransactionService;
import io.onedev.server.service.AuditService;
import io.onedev.server.service.GroupEntitlementService;
import io.onedev.server.service.GroupService;
import io.onedev.server.service.ProjectEntitlementService;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.UserEntitlementService;
import io.onedev.server.service.UserService;
import io.onedev.server.web.editable.BeanContext;
public class EntitlementSettingPanel extends GenericPanel<User> {
@Inject
private UserService userService;
@Inject
private AuditService auditService;
@Inject
private TransactionService transactionService;
@Inject
private ProjectEntitlementService projectEntitlementService;
@Inject
private GroupEntitlementService groupEntitlementService;
@Inject
private UserEntitlementService userEntitlementService;
@Inject
private ProjectService projectService;
@Inject
private GroupService groupService;
public EntitlementSettingPanel(String id, IModel<User> model) {
super(id, model);
}
@Override
protected void onInitialize() {
super.onInitialize();
var bean = new EntitlementEditBean();
bean.setEntitleToAll(getUser().getAiSetting().isEntitleToAll());
bean.setEntitledProjects(getUser().getProjectEntitlements().stream()
.map(it->it.getProject().getPath())
.sorted()
.collect(Collectors.toList()));
bean.setEntitledGroups(getUser().getGroupEntitlements().stream()
.map(it->it.getGroup().getName())
.sorted()
.collect(Collectors.toList()));
bean.setEntitledUsers(getUser().getUserEntitlements().stream()
.map(it->it.getUser().getName())
.sorted()
.collect(Collectors.toList()));
var oldAuditContent = VersionedXmlDoc.fromBean(bean).toXML();
Form<?> form = new Form<Void>("form") {
@Override
protected void onSubmit() {
super.onSubmit();
transactionService.run(() -> {
var newAuditContent = VersionedXmlDoc.fromBean(bean).toXML();
getUser().getAiSetting().setEntitleToAll(bean.isEntitleToAll());
var projectEntitlements = bean.getEntitledProjects().stream().map(it->{
var entitlement = new ProjectEntitlement();
entitlement.setProject(projectService.findByPath(it));
entitlement.setAi(getUser());
return entitlement;
}).collect(Collectors.toList());
var groupEntitlements = bean.getEntitledGroups().stream().map(it->{
var entitlement = new GroupEntitlement();
entitlement.setGroup(groupService.find(it));
entitlement.setAi(getUser());
return entitlement;
}).collect(Collectors.toList());
var userEntitlements = bean.getEntitledUsers().stream().map(it->{
var entitlement = new UserEntitlement();
entitlement.setUser(userService.findByName(it));
entitlement.setAI(getUser());
return entitlement;
}).collect(Collectors.toList());
userService.update(getUser(), null);
projectEntitlementService.syncEntitlements(getUser(), projectEntitlements);
groupEntitlementService.syncEntitlements(getUser(), groupEntitlements);
userEntitlementService.syncEntitlements(getUser(), userEntitlements);
auditService.audit(null, "changed AI entitlement settings", oldAuditContent, newAuditContent);
});
getSession().success(_T("AI entitlement settings have been saved"));
setResponsePage(getPage().getClass(), getPage().getPageParameters());
}
};
form.add(BeanContext.edit("editor", bean));
add(form);
}
private User getUser() {
return getModelObject();
}
}

View File

@ -0,0 +1,6 @@
<wicket:panel>
<form wicket:id="form" class="leave-confirm">
<div wicket:id="editor" class="mb-4"></div>
<input type="submit" class="btn btn-primary dirty-aware" t:value="Save Settings">
</form>
</wicket:panel>

View File

@ -0,0 +1,60 @@
package io.onedev.server.web.component.user.aisetting;
import static io.onedev.server.web.translation.Translation._T;
import javax.inject.Inject;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.panel.GenericPanel;
import org.apache.wicket.model.IModel;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.model.User;
import io.onedev.server.model.support.AiModelSetting;
import io.onedev.server.service.AuditService;
import io.onedev.server.service.UserService;
import io.onedev.server.web.editable.BeanContext;
public class ModelSettingPanel extends GenericPanel<User> {
@Inject
private UserService userService;
@Inject
private AuditService auditService;
public ModelSettingPanel(String id, IModel<User> model) {
super(id, model);
}
@Override
protected void onInitialize() {
super.onInitialize();
AiModelSetting setting = getUser().getAiSetting().getModelSetting();
var oldAuditContent = VersionedXmlDoc.fromBean(setting).toXML();
Form<?> form = new Form<Void>("form") {
@Override
protected void onSubmit() {
super.onSubmit();
var newAuditContent = VersionedXmlDoc.fromBean(setting).toXML();
getUser().getAiSetting().setModelSetting(setting);
userService.update(getUser(), null);
auditService.audit(null, "changed AI model settings", oldAuditContent, newAuditContent);
getSession().success(_T("AI model settings have been saved"));
setResponsePage(getPage().getClass(), getPage().getPageParameters());
}
};
form.add(BeanContext.edit("editor", setting));
add(form);
}
private User getUser() {
return getModelObject();
}
}

View File

@ -1,10 +1,13 @@
package io.onedev.server.web.component.user.basicsetting;
import static io.onedev.server.model.User.PROP_NOTIFY_OWN_EVENTS;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.web.translation.Translation._T;
import java.io.Serializable;
import javax.inject.Inject;
import org.apache.wicket.Session;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.form.Form;
@ -13,11 +16,10 @@ import org.apache.wicket.model.IModel;
import com.google.common.collect.Sets;
import io.onedev.server.OneDev;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.model.User;
import io.onedev.server.service.AuditService;
import io.onedev.server.service.UserService;
import io.onedev.server.model.User;
import io.onedev.server.util.Path;
import io.onedev.server.util.PathNode;
import io.onedev.server.web.editable.BeanContext;
@ -26,6 +28,12 @@ import io.onedev.server.web.page.user.UserPage;
public class BasicSettingPanel extends GenericPanel<User> {
@Inject
private UserService userService;
@Inject
private AuditService auditService;
private BeanEditor editor;
private String oldName;
@ -42,8 +50,8 @@ public class BasicSettingPanel extends GenericPanel<User> {
protected void onInitialize() {
super.onInitialize();
var excludeProperties = Sets.newHashSet("password", "serviceAccount");
if (getUser().isServiceAccount() || getUser().isDisabled())
var excludeProperties = Sets.newHashSet("password", "type", "aiModelSetting");
if (getUser().getType() != ORDINARY || getUser().isDisabled())
excludeProperties.add(PROP_NOTIFY_OWN_EVENTS);
editor = BeanContext.editModel("editor", new IModel<Serializable>() {
@ -75,7 +83,7 @@ public class BasicSettingPanel extends GenericPanel<User> {
User user = getUser();
User userWithSameName = getUserService().findByName(user.getName());
User userWithSameName = userService.findByName(user.getName());
if (userWithSameName != null && !userWithSameName.equals(user)) {
editor.error(new Path(new PathNode.Named(User.PROP_NAME)),
_T("Login name already used by another account"));
@ -83,9 +91,9 @@ public class BasicSettingPanel extends GenericPanel<User> {
if (editor.isValid()) {
var newAuditContent = VersionedXmlDoc.fromBean(editor.getPropertyValues()).toXML();
getUserService().update(user, oldName);
userService.update(user, oldName);
if (getPage() instanceof UserPage)
getAuditService().audit(null, "changed basic settings of account \"" + user.getName() + "\"", oldAuditContent, newAuditContent);
auditService.audit(null, "changed basic settings of account \"" + user.getName() + "\"", oldAuditContent, newAuditContent);
Session.get().success(_T("Basic settings updated"));
setResponsePage(getPage().getClass(), getPage().getPageParameters());
}
@ -98,13 +106,5 @@ public class BasicSettingPanel extends GenericPanel<User> {
add(form);
}
private UserService getUserService() {
return OneDev.getInstance(UserService.class);
}
private AuditService getAuditService() {
return OneDev.getInstance(AuditService.class);
}
}

View File

@ -55,12 +55,18 @@
<wicket:fragment wicket:id="serviceAccountFrag">
<div class="alert alert-light p-2 font-size-sm"><wicket:t>This is a service account for task automation purpose</wicket:t></div>
</wicket:fragment>
<wicket:fragment wicket:id="aiAccountFrag">
<div class="alert alert-light p-2 font-size-sm"><wicket:t>This is an AI account</wicket:t></div>
</wicket:fragment>
<wicket:fragment wicket:id="disabledFrag">
<div class="alert alert-light-warning p-2 font-size-sm"><wicket:t>This account is disabled</wicket:t></div>
</wicket:fragment>
<wicket:fragment wicket:id="disabledServiceAccountFrag">
<div class="alert alert-light-warning p-2 font-size-sm"><wicket:t>This is a disabled service account</wicket:t></div>
</wicket:fragment>
<wicket:fragment wicket:id="disabledAIAccountFrag">
<div class="alert alert-light-warning p-2 font-size-sm"><wicket:t>This is a disabled AI account</wicket:t></div>
</wicket:fragment>
<wicket:fragment wicket:id="dateFrag">
<h6 class="mt-4 mb-3 d-flex align-items-center">
<wicket:svg href="calendar" class="icon mr-2"/> <span wicket:id="date"></span>

View File

@ -1,5 +1,7 @@
package io.onedev.server.web.component.user.profile;
import static io.onedev.server.model.User.Type.AI;
import static io.onedev.server.model.User.Type.SERVICE;
import static io.onedev.server.web.translation.Translation._T;
import java.text.MessageFormat;
@ -219,13 +221,17 @@ public abstract class UserProfilePanel extends GenericPanel<User> {
}
WebMarkupContainer noteContainer;
if (user.isDisabled() && WicketUtils.isSubscriptionActive()) {
if (user.isServiceAccount())
if (user.isDisabled()) {
if (user.getType() == SERVICE)
noteContainer = new Fragment("note", "disabledServiceAccountFrag", this);
else if (user.getType() == AI)
noteContainer = new Fragment("note", "disabledAIAccountFrag", this);
else
noteContainer = new Fragment("note", "disabledFrag", this);
} else if (user.isServiceAccount() && WicketUtils.isSubscriptionActive()) {
} else if (user.getType() == SERVICE) {
noteContainer = new Fragment("note", "serviceAccountFrag", this);
} else if (user.getType() == AI) {
noteContainer = new Fragment("note", "aiAccountFrag", this);
} else if (user.getPassword() != null) {
noteContainer = new Fragment("note", "authViaInternalDatabaseFrag", this);
var actions = new WebMarkupContainer("actions");

View File

@ -1,3 +1,3 @@
<wicket:panel>
<input wicket:id="input" type="hidden"></input>
<input wicket:id="input" type="hidden">
</wicket:panel>

View File

@ -1,3 +1,3 @@
<wicket:panel>
<input wicket:id="input" type="hidden"></input>
<input wicket:id="input" type="hidden">
</wicket:panel>

View File

@ -77,6 +77,8 @@ import io.onedev.server.web.page.help.ResourceDetailPage;
import io.onedev.server.web.page.help.ResourceListPage;
import io.onedev.server.web.page.issues.IssueListPage;
import io.onedev.server.web.page.my.accesstoken.MyAccessTokensPage;
import io.onedev.server.web.page.my.aisetting.MyEntitlementSettingPage;
import io.onedev.server.web.page.my.aisetting.MyModelSettingPage;
import io.onedev.server.web.page.my.avatar.MyAvatarPage;
import io.onedev.server.web.page.my.basicsetting.MyBasicSettingPage;
import io.onedev.server.web.page.my.emailaddresses.MyEmailAddressesPage;
@ -163,6 +165,8 @@ import io.onedev.server.web.page.security.SsoProcessPage;
import io.onedev.server.web.page.serverinit.ServerInitPage;
import io.onedev.server.web.page.test.TestPage;
import io.onedev.server.web.page.user.accesstoken.UserAccessTokensPage;
import io.onedev.server.web.page.user.aisetting.UserEntitlementSettingPage;
import io.onedev.server.web.page.user.aisetting.UserModelSettingPage;
import io.onedev.server.web.page.user.avatar.UserAvatarPage;
import io.onedev.server.web.page.user.basicsetting.UserBasicSettingPage;
import io.onedev.server.web.page.user.emailaddresses.UserEmailAddressesPage;
@ -227,6 +231,8 @@ public class BaseUrlMapper extends CompoundRequestMapper {
add(new BasePageMapper("~my/email-addresses", MyEmailAddressesPage.class));
add(new BasePageMapper("~my/avatar", MyAvatarPage.class));
add(new BasePageMapper("~my/password", MyPasswordPage.class));
add(new BasePageMapper("~my/ai-model-setting", MyModelSettingPage.class));
add(new BasePageMapper("~my/ai-entitlement-setting", MyEntitlementSettingPage.class));
add(new BasePageMapper("~my/ssh-keys", MySshKeysPage.class));
add(new BasePageMapper("~my/gpg-keys", MyGpgKeysPage.class));
add(new BasePageMapper("~my/access-tokens", MyAccessTokensPage.class));
@ -286,6 +292,8 @@ public class BaseUrlMapper extends CompoundRequestMapper {
io.onedev.server.web.page.user.authorization.UserAuthorizationsPage.class));
add(new BasePageMapper("~users/${user}/avatar", UserAvatarPage.class));
add(new BasePageMapper("~users/${user}/password", UserPasswordPage.class));
add(new BasePageMapper("~users/${user}/ai-model-setting", UserModelSettingPage.class));
add(new BasePageMapper("~users/${user}/ai-entitlement-setting", UserEntitlementSettingPage.class));
add(new BasePageMapper("~users/${user}/ssh-keys", UserSshKeysPage.class));
add(new BasePageMapper("~users/${user}/gpg-keys", UserGpgKeysPage.class));
add(new BasePageMapper("~users/${user}/access-tokens", UserAccessTokensPage.class));

View File

@ -53,4 +53,13 @@
}
.branding-setting .dark-mode>.logo-preview>div {
background-color: var(--dark-mode-dark) !important;
}
}
.property-aiModelSetting>.value {
margin-top: 1rem;
padding: 0 0 0 1rem;
border-left: 3px solid var(--light-gray);
}
.dark-mode .property-aiModelSetting>.value {
border-color: var(--dark-mode-light-dark);
}

View File

@ -10,7 +10,7 @@ import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.model.support.administration.AISetting;
import io.onedev.server.model.support.administration.AiSetting;
import io.onedev.server.service.SettingService;
import io.onedev.server.web.editable.PropertyContext;
import io.onedev.server.web.page.admin.AdministrationPage;
@ -28,7 +28,7 @@ public class LiteModelPage extends AdministrationPage {
protected void onInitialize() {
super.onInitialize();
AISetting aiSetting = settingService.getAISetting();
AiSetting aiSetting = settingService.getAiSetting();
var oldAuditContent = VersionedXmlDoc.fromBean(aiSetting).toXML();
Form<?> form = new Form<Void>("form") {
@ -37,7 +37,7 @@ public class LiteModelPage extends AdministrationPage {
protected void onSubmit() {
super.onSubmit();
var newAuditContent = VersionedXmlDoc.fromBean(aiSetting).toXML();
settingService.saveAISetting(aiSetting);
settingService.saveAiSetting(aiSetting);
auditService.audit(null, "changed AI settings", oldAuditContent, newAuditContent);
getSession().success(_T("Lite AI model settings have been saved"));
@ -45,7 +45,7 @@ public class LiteModelPage extends AdministrationPage {
}
};
form.add(PropertyContext.edit("editor", aiSetting, AISetting.PROP_LITE_MODEL_SETTING));
form.add(PropertyContext.edit("editor", aiSetting, AiSetting.PROP_LITE_MODEL_SETTING));
add(form);
}

View File

@ -1,5 +1,6 @@
package io.onedev.server.web.page.admin.usermanagement;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.web.translation.Translation._T;
import org.apache.shiro.authc.credential.PasswordService;
@ -18,15 +19,16 @@ import com.google.common.collect.Sets;
import io.onedev.commons.loader.AppLoader;
import io.onedev.server.OneDev;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.service.EmailAddressService;
import io.onedev.server.service.MembershipService;
import io.onedev.server.service.SettingService;
import io.onedev.server.service.UserService;
import io.onedev.server.model.EmailAddress;
import io.onedev.server.model.Group;
import io.onedev.server.model.Membership;
import io.onedev.server.model.User;
import io.onedev.server.model.support.AiSetting;
import io.onedev.server.persistence.TransactionService;
import io.onedev.server.service.EmailAddressService;
import io.onedev.server.service.MembershipService;
import io.onedev.server.service.SettingService;
import io.onedev.server.service.UserService;
import io.onedev.server.util.Path;
import io.onedev.server.util.PathNode;
import io.onedev.server.web.editable.BeanContext;
@ -63,7 +65,7 @@ public class NewUserPage extends AdministrationPage {
_T("User name already used by another account"));
}
if (!bean.isServiceAccount() && getEmailAddressService().findByValue(bean.getEmailAddress()) != null) {
if (bean.getType() == ORDINARY && getEmailAddressService().findByValue(bean.getEmailAddress()) != null) {
editor.error(new Path(new PathNode.Named(NewUserBean.PROP_EMAIL_ADDRESS)),
_T("Email address already used by another user"));
}
@ -71,9 +73,12 @@ public class NewUserPage extends AdministrationPage {
User user = new User();
user.setName(bean.getName());
user.setFullName(bean.getFullName());
user.setServiceAccount(bean.isServiceAccount());
user.setType(bean.getType());
var aiSetting = new AiSetting();
aiSetting.setModelSetting(bean.getAiModelSetting());
user.setAiSetting(aiSetting);
var defaultLoginGroup = getSettingService().getSecuritySetting().getDefaultGroup();
if (user.isServiceAccount()) {
if (user.getType() != ORDINARY) {
getTransactionService().run(new Runnable() {
@Override
public void run() {

View File

@ -24,7 +24,8 @@
<a wicket:id="link">
<img wicket:id="avatar">
<span wicket:id="name" class="name"></span>
<span wicket:id="service" class="badge badge-info badge-sm ml-1"><wicket:t>service</wicket:t></span>
<span wicket:id="service" class="badge badge-info badge-sm ml-1">service</span>
<span wicket:id="ai" class="badge badge-info badge-sm ml-1">AI</span>
<span wicket:id="disabled" class="badge badge-warning badge-sm ml-1"><wicket:t>disabled</wicket:t></span>
</a>
</wicket:fragment>

View File

@ -1,5 +1,8 @@
package io.onedev.server.web.page.admin.usermanagement;
import static io.onedev.server.model.User.Type.AI;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.model.User.Type.SERVICE;
import static io.onedev.server.web.translation.Translation._T;
import java.io.Serializable;
@ -89,7 +92,7 @@ public class UserListPage extends AdministrationPage {
var emailAddresses = new HashMap<Long, Collection<String>>();
for (var emailAddress: emailAddressCache.values())
emailAddresses.computeIfAbsent(emailAddress.getOwnerId(), key -> new HashSet<>()).add(emailAddress.getValue());
List<User> users = new ArrayList<>(userCache.getUsers(state.includeDisabled));
List<User> users = new ArrayList<>(userCache.getUsers(it->state.includeDisabled?true:!it.isDisabled()));
users.sort(userCache.comparingDisplayName(Sets.newHashSet()));
users = new Similarities<>(users) {
@ -852,7 +855,8 @@ public class UserListPage extends AdministrationPage {
};
link.add(new UserAvatar("avatar", user));
link.add(new Label("name", user.getName()));
link.add(new WebMarkupContainer("service").setVisible(user.isServiceAccount() && WicketUtils.isSubscriptionActive()));
link.add(new WebMarkupContainer("service").setVisible(user.getType() == SERVICE));
link.add(new WebMarkupContainer("ai").setVisible(user.getType() == AI));
link.add(new WebMarkupContainer("disabled").setVisible(user.isDisabled() && WicketUtils.isSubscriptionActive()));
fragment.add(link);
cellItem.add(fragment);
@ -879,7 +883,7 @@ public class UserListPage extends AdministrationPage {
@Override
public void populateItem(Item<ICellPopulator<User>> cellItem, String componentId,
IModel<User> rowModel) {
if (rowModel.getObject().isServiceAccount()) {
if (rowModel.getObject().getType() != ORDINARY) {
cellItem.add(new Label(componentId, "<i>N/A</i>").setEscapeModelStrings(false));
} else {
EmailAddress emailAddress = rowModel.getObject().getPrimaryEmailAddress();
@ -906,7 +910,7 @@ public class UserListPage extends AdministrationPage {
@Override
public void populateItem(Item<ICellPopulator<User>> cellItem, String componentId, IModel<User> rowModel) {
if (rowModel.getObject().isServiceAccount() || rowModel.getObject().isDisabled()) {
if (rowModel.getObject().getType() != ORDINARY || rowModel.getObject().isDisabled()) {
cellItem.add(new Label(componentId, "<i>" + _T("N/A") + "</i>").setEscapeModelStrings(false));
} else {
cellItem.add(new Label(componentId, _T(rowModel.getObject().getAuthSource())));

View File

@ -216,7 +216,7 @@ onedev.server = {
onedev.server.focus.$components = null;
},
doFocus: function($containers) {
doFocus: function($containers) {
/*
* Do focus with a timeout as otherwise it will not work in a panel replaced
* via Wicket
@ -249,19 +249,24 @@ onedev.server = {
$containers.find(focusibleSelector).addBack(focusibleSelector).filter(":visible").each(function() {
var $this = $(this);
if ($this.closest(".no-autofocus").length == 0) {
var focused = false;
if ($this.hasClass("CodeMirror") && $this[0].CodeMirror.options.readOnly == false) {
$this[0].CodeMirror.focus();
focused = true;
} else if ($this.attr("readonly") != "readonly" && $this.attr("disabled") != "disabled") {
if ($this.closest(".select2-container").length != 0) {
if ($this.closest(".inplace-property-edit").length != 0) {
$this.focus();
focused = true;
$this.closest(".select2-container").next("input").select2("open");
}
} else {
$this.focus();
focused = true;
}
}
return false;
if (focused)
return false;
}
});
}

View File

@ -47,6 +47,7 @@
<div class="topbar-right text-nowrap">
<a wicket:id="alerts" t:data-tippy-content="Alerts" class="text-warning topbar-link"><wicket:svg href="exclamation-circle-o" class="icon fade-in-out"></wicket:svg></a>
<a wicket:id="newVersionStatus" t:data-tippy-content="New version available. Red for security/critical update, yellow for bug fix, blue for feature update. Click to show changes. Disable in system setting" class="new-version-status topbar-link"><img wicket:id="icon"/></a>
<a wicket:id="showChat" t:data-tippy-content="Chat with AI" class="topbar-link"><wicket:svg href="ai" class="icon"/></a>
<a wicket:id="darkMode" t:data-tippy-content="Toggle dark mode" class="topbar-link"><svg wicket:id="icon" class="icon"/></a>
<a wicket:id="showCommandPalette" class="topbar-link command-palette"></a>
<a wicket:id="languageSelector" t:data-tippy-content="Language" class="topbar-link language-selector"><wicket:svg href="globe" class="icon"></wicket:svg></a>
@ -87,6 +88,10 @@
<wicket:svg href="password" class="icon mr-2"></wicket:svg>
<wicket:t>Password</wicket:t>
</a>
<a wicket:id="myAISetting" class="dropdown-item">
<wicket:svg href="ai-setting" class="icon mr-2"></wicket:svg>
<wicket:t>AI Settings</wicket:t>
</a>
<a wicket:id="mySshKeys" class="dropdown-item">
<wicket:svg href="key" class="icon mr-2"></wicket:svg>
<wicket:t>SSH Keys</wicket:t>
@ -117,6 +122,7 @@
</div>
<div class="main autofit d-flex flex-column resize-aware">
<wicket:child></wicket:child>
<div wicket:id="chat"></div>
</div>
<wicket:fragment wicket:id="menuHeaderFrag">

View File

@ -1,6 +1,8 @@
package io.onedev.server.web.page.layout;
import static io.onedev.server.model.Alert.PROP_DATE;
import static io.onedev.server.model.User.Type.AI;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.web.translation.Translation._T;
import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;
@ -76,6 +78,7 @@ import io.onedev.server.util.DateUtils;
import io.onedev.server.web.WebConstants;
import io.onedev.server.web.behavior.AbstractPostAjaxBehavior;
import io.onedev.server.web.behavior.ChangeObserver;
import io.onedev.server.web.component.ai.chat.ChatPanel;
import io.onedev.server.web.component.brandlogo.BrandLogoPanel;
import io.onedev.server.web.component.commandpalette.CommandPalettePanel;
import io.onedev.server.web.component.datatable.DefaultDataTable;
@ -151,6 +154,7 @@ import io.onedev.server.web.page.base.BasePage;
import io.onedev.server.web.page.help.IncompatibilitiesPage;
import io.onedev.server.web.page.my.MyPage;
import io.onedev.server.web.page.my.accesstoken.MyAccessTokensPage;
import io.onedev.server.web.page.my.aisetting.MyModelSettingPage;
import io.onedev.server.web.page.my.avatar.MyAvatarPage;
import io.onedev.server.web.page.my.basicsetting.MyBasicSettingPage;
import io.onedev.server.web.page.my.emailaddresses.MyEmailAddressesPage;
@ -807,6 +811,24 @@ public abstract class LayoutPage extends BasePage {
});
var chat = new ChatPanel("chat");
add(chat);
topbar.add(new AjaxLink<Void>("showChat") {
@Override
public void onClick(AjaxRequestTarget target) {
chat.show(target);
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(getLoginUser() != null && !getLoginUser().getEntitledAis().isEmpty());
}
});
topbar.add(new MenuLink("languageSelector") {
private void switchLanguage(Locale locale) {
@ -1061,7 +1083,7 @@ public abstract class LayoutPage extends BasePage {
if (loginUser != null) {
userInfo.add(new UserAvatar("avatar", loginUser));
userInfo.add(new Label("name", loginUser.getDisplayName()));
if (loginUser.isServiceAccount() || loginUser.isDisabled()) {
if (loginUser.getType() != ORDINARY || loginUser.isDisabled()) {
userInfo.add(new WebMarkupContainer("hasUnverifiedLink").setVisible(false));
userInfo.add(new WebMarkupContainer("noPrimaryAddressLink").setVisible(false));
} else if (loginUser.getEmailAddresses().isEmpty()) {
@ -1088,7 +1110,7 @@ public abstract class LayoutPage extends BasePage {
if (getPage() instanceof MyBasicSettingPage)
item.add(AttributeAppender.append("class", "active"));
if (getLoginUser() != null && !getLoginUser().isServiceAccount()) {
if (getLoginUser() != null && getLoginUser().getType() == ORDINARY) {
userInfo.add(item = new ViewStateAwarePageLink<Void>("myEmailSetting", MyEmailAddressesPage.class));
if (getPage() instanceof MyEmailAddressesPage)
item.add(AttributeAppender.append("class", "active"));
@ -1100,7 +1122,7 @@ public abstract class LayoutPage extends BasePage {
if (getPage() instanceof MyAvatarPage)
item.add(AttributeAppender.append("class", "active"));
if (loginUser != null && loginUser.getPassword() != null && !loginUser.isServiceAccount() && !loginUser.isDisabled()) {
if (loginUser != null && loginUser.getPassword() != null && loginUser.getType() == ORDINARY && !loginUser.isDisabled()) {
userInfo.add(item = new ViewStateAwarePageLink<Void>("myPassword", MyPasswordPage.class));
if (getPage() instanceof MyPasswordPage)
item.add(AttributeAppender.append("class", "active"));
@ -1108,6 +1130,14 @@ public abstract class LayoutPage extends BasePage {
userInfo.add(new WebMarkupContainer("myPassword").setVisible(false));
}
if (loginUser != null && loginUser.getType() == AI && !loginUser.isDisabled()) {
userInfo.add(item = new ViewStateAwarePageLink<Void>("myAISetting", MyModelSettingPage.class));
if (getPage() instanceof MyModelSettingPage)
item.add(AttributeAppender.append("class", "active"));
} else {
userInfo.add(new WebMarkupContainer("myAISetting").setVisible(false));
}
if (OneDev.getInstance(ServerConfig.class).getSshPort() != 0 && loginUser != null && !loginUser.isDisabled()) {
userInfo.add(item = new ViewStateAwarePageLink<Void>("mySshKeys", MySshKeysPage.class));
if (getPage() instanceof MySshKeysPage)
@ -1132,7 +1162,7 @@ public abstract class LayoutPage extends BasePage {
userInfo.add(new WebMarkupContainer("myAccessTokens").setVisible(false));
}
if (getLoginUser() != null && !getLoginUser().isServiceAccount() && !getLoginUser().isDisabled()) {
if (getLoginUser() != null && getLoginUser().getType() == ORDINARY && !getLoginUser().isDisabled()) {
userInfo.add(item = new ViewStateAwarePageLink<Void>("myTwoFactorAuthentication", MyTwoFactorAuthenticationPage.class) {
@Override
protected void onConfigure() {
@ -1146,7 +1176,7 @@ public abstract class LayoutPage extends BasePage {
userInfo.add(new WebMarkupContainer("myTwoFactorAuthentication").setVisible(false));
}
if (loginUser != null && !loginUser.isDisabled() && !loginUser.isServiceAccount()) {
if (loginUser != null && !loginUser.isDisabled() && loginUser.getType() == ORDINARY) {
userInfo.add(item = new ViewStateAwarePageLink<Void>("mySsoAccounts", MySsoAccountsPage.class));
if (getPage() instanceof MySsoAccountsPage)
item.add(AttributeAppender.append("class", "active"));
@ -1154,7 +1184,7 @@ public abstract class LayoutPage extends BasePage {
userInfo.add(new WebMarkupContainer("mySsoAccounts").setVisible(false));
}
if (getLoginUser() != null && !getLoginUser().isServiceAccount() && !getLoginUser().isDisabled()) {
if (getLoginUser() != null && getLoginUser().getType() == ORDINARY && !getLoginUser().isDisabled()) {
userInfo.add(item = new ViewStateAwarePageLink<Void>("myQueryWatches", MyQueryWatchesPage.class));
if (getPage() instanceof MyQueryWatchesPage)
item.add(AttributeAppender.append("class", "active"));
@ -1218,6 +1248,7 @@ public abstract class LayoutPage extends BasePage {
getUpdateCheckService().cacheNewVersionStatus(newVersionStatus);
}
});
}
private SubscriptionService getSubscriptionService() {

View File

@ -306,6 +306,8 @@
.topbar-right .new-version-status>img {
margin-top: -3px;
}
.topbar-right a.topbar-link.active,
.topbar-right a.topbar-link:hover,
.topbar-right .active>a.topbar-link,
.topbar-right .show>a.topbar-link {

View File

@ -0,0 +1,4 @@
<wicket:extend>
<ul wicket:id="aiSettingTabs" class="tabs nav nav-bold nav-tabs nav-tabs-line text-weight-bold mb-4"></ul>
<wicket:child></wicket:child>
</wicket:extend>

View File

@ -0,0 +1,43 @@
package io.onedev.server.web.page.my.aisetting;
import static io.onedev.server.model.User.Type.AI;
import static io.onedev.server.web.translation.Translation._T;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.web.component.tabbable.PageTab;
import io.onedev.server.web.component.tabbable.Tabbable;
import io.onedev.server.web.page.my.MyPage;
public abstract class MyAiSettingPage extends MyPage {
public MyAiSettingPage(PageParameters params) {
super(params);
if (getUser().isDisabled() || getUser().getType() != AI)
throw new IllegalStateException();
}
@Override
protected void onInitialize() {
super.onInitialize();
List<PageTab> tabs = new ArrayList<>();
tabs.add(new PageTab(Model.of(_T("Model")), null, MyModelSettingPage.class, new PageParameters()));
tabs.add(new PageTab(Model.of(_T("Entitlement")), null, MyEntitlementSettingPage.class, new PageParameters()));
add(new Tabbable("aiSettingTabs", tabs));
}
@Override
protected Component newTopbarTitle(String componentId) {
return new Label(componentId, _T("My AI Settings"));
}
}

View File

@ -0,0 +1,3 @@
<wicket:extend>
<div wicket:id="content"></div>
</wicket:extend>

View File

@ -0,0 +1,29 @@
package io.onedev.server.web.page.my.aisetting;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.model.User;
import io.onedev.server.web.component.user.aisetting.EntitlementSettingPanel;
public class MyEntitlementSettingPage extends MyAiSettingPage {
public MyEntitlementSettingPage(PageParameters params) {
super(params);
}
@Override
protected void onInitialize() {
super.onInitialize();
add(new EntitlementSettingPanel("content", new AbstractReadOnlyModel<User>() {
@Override
public User getObject() {
return getUser();
}
}));
}
}

View File

@ -0,0 +1,3 @@
<wicket:extend>
<div wicket:id="content"></div>
</wicket:extend>

View File

@ -0,0 +1,29 @@
package io.onedev.server.web.page.my.aisetting;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.model.User;
import io.onedev.server.web.component.user.aisetting.ModelSettingPanel;
public class MyModelSettingPage extends MyAiSettingPage {
public MyModelSettingPage(PageParameters params) {
super(params);
}
@Override
protected void onInitialize() {
super.onInitialize();
add(new ModelSettingPanel("content", new AbstractReadOnlyModel<User>() {
@Override
public User getObject() {
return getUser();
}
}));
}
}

View File

@ -1,5 +1,6 @@
package io.onedev.server.web.page.my.emailaddresses;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.web.translation.Translation._T;
import org.apache.wicket.Component;
@ -15,7 +16,7 @@ public class MyEmailAddressesPage extends MyPage {
public MyEmailAddressesPage(PageParameters params) {
super(params);
if (getUser().isServiceAccount())
if (getUser().getType() != ORDINARY)
throw new IllegalStateException();
}

View File

@ -5,6 +5,7 @@ import io.onedev.server.model.User;
import io.onedev.server.web.component.user.passwordedit.PasswordEditPanel;
import io.onedev.server.web.page.my.MyPage;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.web.translation.Translation._T;
import org.apache.wicket.Component;
@ -16,7 +17,7 @@ public class MyPasswordPage extends MyPage {
public MyPasswordPage(PageParameters params) {
super(params);
if (getUser().isServiceAccount() || getUser().isDisabled())
if (getUser().getType() != ORDINARY || getUser().isDisabled())
throw new IllegalStateException();
if (getUser().getPassword() == null)
throw new ExplicitException(_T("Unable to change password as you are authenticating via external system"));
@ -28,12 +29,12 @@ public class MyPasswordPage extends MyPage {
add(new PasswordEditPanel("content", new AbstractReadOnlyModel<User>() {
@Override
public User getObject() {
return getUser();
}
}));
@Override
public User getObject() {
return getUser();
}
}));
}
@Override

View File

@ -1,5 +1,6 @@
package io.onedev.server.web.page.my.querywatch;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.web.translation.Translation._T;
import org.apache.wicket.Component;
@ -20,7 +21,7 @@ public class MyQueryWatchesPage extends MyPage {
public MyQueryWatchesPage(PageParameters params) {
super(params);
if (getUser().isServiceAccount() || getUser().isDisabled())
if (getUser().getType() != ORDINARY || getUser().isDisabled())
throw new IllegalStateException();
tabName = params.get(PARAM_TAB).toString(QueryWatchesPanel.TAB_ISSUE);
}

View File

@ -1,5 +1,6 @@
package io.onedev.server.web.page.my.ssoaccounts;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.web.translation.Translation._T;
import org.apache.wicket.Component;
@ -15,7 +16,7 @@ public class MySsoAccountsPage extends MyPage {
public MySsoAccountsPage(PageParameters params) {
super(params);
if (getUser().isDisabled() || getUser().isServiceAccount())
if (getUser().isDisabled() || getUser().getType() != ORDINARY)
throw new IllegalStateException();
}

View File

@ -5,6 +5,7 @@ import io.onedev.server.model.User;
import io.onedev.server.web.component.user.twofactorauthentication.TwoFactorAuthenticationStatusPanel;
import io.onedev.server.web.page.my.MyPage;
import static io.onedev.server.model.User.Type.ORDINARY;
import static io.onedev.server.web.translation.Translation._T;
import org.apache.wicket.Component;
@ -15,7 +16,7 @@ public class MyTwoFactorAuthenticationPage extends MyPage {
public MyTwoFactorAuthenticationPage(PageParameters params) {
super(params);
if (getUser().isServiceAccount() || getUser().isDisabled())
if (getUser().getType() != ORDINARY || getUser().isDisabled())
throw new IllegalStateException();
else if (!getUser().isEnforce2FA())
throw new ExplicitException(_T("Two-factor authentication not enabled"));

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