GUI whitelabel runtime system (#8942)

* Add first version

* Add guithemedetails join

* Update since and remove extra line

* Limit information on API response for non admin users

* Add base files for preset themes

* Add miising license

* Revert cookie check

* Fix imports

* Fix pre-commit

* Address log4j2 string to format review and add license to css files

* Fix infinite loading

* Move event details to service implementation

* Move view to a specific view file

* Refactoring gui theme classes

* Normalize package name

* Address Henrique review

* Fix create table SQL

* Add interface for Dao classes

* Remove extra tabs

* Address unauthorized call when 2FA is enabled

* Remove trailing whitespaces

* Apply suggestions from code review

Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>

---------

Co-authored-by: Henrique Sato <henriquesato2003@gmail.com>
Co-authored-by: Bernardo De Marco Gonçalves <bernardomg2004@gmail.com>
Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>
This commit is contained in:
Bryan Lima 2025-07-15 09:59:09 +02:00 committed by GitHub
parent 6059724189
commit fb6adacc51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 5519 additions and 20 deletions

View File

@ -809,6 +809,11 @@ public class EventTypes {
public static final String VM_LEASE_CANCELLED = "VM.LEASE.CANCELLED";
public static final String VM_LEASE_EXPIRING = "VM.LEASE.EXPIRING";
// GUI Theme
public static final String EVENT_GUI_THEME_CREATE = "GUI.THEME.CREATE";
public static final String EVENT_GUI_THEME_REMOVE = "GUI.THEME.REMOVE";
public static final String EVENT_GUI_THEME_UPDATE = "GUI.THEME.UPDATE";
static {
// TODO: need a way to force author adding event types to declare the entity details as well, with out braking
@ -1312,6 +1317,11 @@ public class EventTypes {
entityEventDetails.put(VM_LEASE_EXPIRING, VirtualMachine.class);
entityEventDetails.put(VM_LEASE_DISABLED, VirtualMachine.class);
entityEventDetails.put(VM_LEASE_CANCELLED, VirtualMachine.class);
// GUI theme
entityEventDetails.put(EVENT_GUI_THEME_CREATE, "GuiTheme");
entityEventDetails.put(EVENT_GUI_THEME_REMOVE, "GuiTheme");
entityEventDetails.put(EVENT_GUI_THEME_UPDATE, "GuiTheme");
}
public static boolean isNetworkEvent(String eventType) {

View File

@ -1256,6 +1256,22 @@ public class ApiConstants {
public static final String VMWARE_DC = "vmwaredc";
public static final String CSS = "css";
public static final String JSON_CONFIGURATION = "jsonconfiguration";
public static final String COMMON_NAMES = "commonnames";
public static final String COMMON_NAME = "commonname";
public static final String DOMAIN_IDS = "domainids";
public static final String SHOW_PUBLIC = "showpublic";
public static final String LIST_ONLY_DEFAULT_THEME = "listonlydefaulttheme";
public static final String RECURSIVE_DOMAINS = "recursivedomains";
/**
* This enum specifies IO Drivers, each option controls specific policies on I/O.
* Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0).

View File

@ -64,6 +64,7 @@ import org.apache.cloudstack.api.response.GuestOSResponse;
import org.apache.cloudstack.api.response.GuestOsMappingResponse;
import org.apache.cloudstack.api.response.GuestVlanRangeResponse;
import org.apache.cloudstack.api.response.GuestVlanResponse;
import org.apache.cloudstack.api.response.GuiThemeResponse;
import org.apache.cloudstack.api.response.HostForMigrationResponse;
import org.apache.cloudstack.api.response.HostResponse;
import org.apache.cloudstack.api.response.HypervisorCapabilitiesResponse;
@ -150,6 +151,7 @@ import org.apache.cloudstack.config.ConfigurationGroup;
import org.apache.cloudstack.direct.download.DirectDownloadCertificate;
import org.apache.cloudstack.direct.download.DirectDownloadCertificateHostMap;
import org.apache.cloudstack.direct.download.DirectDownloadManager;
import org.apache.cloudstack.gui.theme.GuiThemeJoin;
import org.apache.cloudstack.management.ManagementServerHost;
import org.apache.cloudstack.network.lb.ApplicationLoadBalancerRule;
import org.apache.cloudstack.region.PortableIp;
@ -579,4 +581,6 @@ public interface ResponseGenerator {
SharedFSResponse createSharedFSResponse(ResponseView view, SharedFS sharedFS);
void updateTemplateIsoResponsesForIcons(List<TemplateResponse> responses, ResourceTag.ResourceObjectType type);
GuiThemeResponse createGuiThemeResponse(GuiThemeJoin guiThemeJoin);
}

View File

@ -0,0 +1,129 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.gui.theme;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.GuiThemeResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.gui.theme.GuiTheme;
import org.apache.cloudstack.gui.theme.GuiThemeJoin;
import org.apache.cloudstack.gui.theme.GuiThemeService;
import javax.inject.Inject;
@APICommand(name = "createGuiTheme", description = "Creates a customized GUI theme for a set of Common Names (fixed or wildcard), a set of domain UUIDs, and/or a set of " +
"account UUIDs.", responseObject = GuiThemeResponse.class, entityType = {GuiTheme.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
since = "4.21.0.0", authorized = {RoleType.Admin})
public class CreateGuiThemeCmd extends BaseCmd {
@Inject
GuiThemeService guiThemeService;
@Parameter(name = ApiConstants.NAME, required = true, type = CommandType.STRING, length = 2048, description = "A name to identify the theme.")
private String name;
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, length = 4096, description = "A description for the theme.")
private String description;
@Parameter(name = ApiConstants.CSS, type = CommandType.STRING, length = 65535, description = "The CSS to be retrieved and imported into the GUI " +
"when matching the theme access configurations.")
private String css;
@Parameter(name = ApiConstants.JSON_CONFIGURATION, type = CommandType.STRING, length = 65535, description = "The JSON with the configurations to be " +
"retrieved and imported into the GUI when matching the theme access configurations.")
private String jsonConfiguration;
@Parameter(name = ApiConstants.COMMON_NAMES, type = CommandType.STRING, length = 65535, description = "A set of Common Names (CN) (fixed or " +
"wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com")
private String commonNames;
@Parameter(name = ApiConstants.DOMAIN_IDS, type = CommandType.STRING, length = 65535, description = "A set of domain UUIDs (also known as ID for " +
"the end-user) separated by comma that can retrieve the theme.")
private String domainIds;
@Parameter(name = ApiConstants.ACCOUNT_IDS, type = CommandType.STRING, length = 65535, description = "A set of account UUIDs (also known as ID for" +
" the end-user) separated by comma that can retrieve the theme.")
private String accountIds;
@Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, description = "Defines whether a theme can be retrieved by anyone when only " +
"the `commonNames` is informed. If the `domainIds` or `accountIds` is informed, it is considered as `false`.")
private Boolean isPublic = true;
@Parameter(name = ApiConstants.RECURSIVE_DOMAINS, type = CommandType.BOOLEAN, description = "Defines whether the subdomains of the informed domains are considered. Default " +
"value is false.")
private Boolean recursiveDomains = false;
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getCss() {
return css;
}
public String getJsonConfiguration() {
return jsonConfiguration;
}
public String getCommonNames() {
return commonNames;
}
public String getDomainIds() {
return domainIds;
}
public String getAccountIds() {
return accountIds;
}
public Boolean getPublic() {
return isPublic;
}
public Boolean getRecursiveDomains() {
return recursiveDomains;
}
@Override
public void execute() {
GuiThemeJoin guiThemeJoin = guiThemeService.createGuiTheme(this);
if (guiThemeJoin == null) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create the GUI theme.");
}
GuiThemeResponse response = _responseGenerator.createGuiThemeResponse(guiThemeJoin);
response.setResponseName(getCommandName());
this.setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccountId();
}
}

View File

@ -0,0 +1,110 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.gui.theme;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.GuiThemeResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.gui.theme.GuiTheme;
import org.apache.cloudstack.gui.theme.GuiThemeService;
import javax.inject.Inject;
@APICommand(name = "listGuiThemes", description = "Lists GUI themes.", responseObject = GuiThemeResponse.class, entityType = {GuiTheme.class},
since = "4.21.0.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin, RoleType.User, RoleType.DomainAdmin,
RoleType.ResourceAdmin})
public class ListGuiThemesCmd extends BaseListCmd {
@Inject
GuiThemeService guiThemeService;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, description = "The theme ID.", entityType = GuiThemeResponse.class)
private Long id;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the theme.")
private String name;
@Parameter(name = ApiConstants.COMMON_NAME, type = CommandType.STRING, description = "The internet Common Name (CN) to be filtered.")
private String commonName;
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "The ID of the domain to be filtered.")
private Long domainId;
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "The ID of the account to be filtered.")
private Long accountId;
@Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "Whether to list all themes.")
private boolean listAll = false;
@Parameter(name = ApiConstants.SHOW_REMOVED, type = CommandType.BOOLEAN, description = "Whether to list removed themes.")
private boolean showRemoved = false;
@Parameter(name = ApiConstants.SHOW_PUBLIC, type = CommandType.BOOLEAN, description = "Whether to list public themes.")
private Boolean showPublic;
@Parameter(name = ApiConstants.LIST_ONLY_DEFAULT_THEME, type = CommandType.BOOLEAN, description = "Whether to only list the default theme.")
private boolean listOnlyDefaultTheme = false;
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getCommonName() {
return commonName;
}
public Long getDomainId() {
return domainId;
}
public Long getAccountId() {
return accountId;
}
public boolean getListAll() {
return listAll;
}
public boolean getShowRemoved() {
return showRemoved;
}
public Boolean getShowPublic() {
return showPublic;
}
public boolean getListOnlyDefaultTheme() {
return listOnlyDefaultTheme;
}
@Override
public void execute() {
ListResponse<GuiThemeResponse> response = guiThemeService.listGuiThemes(this);
response.setResponseName(getCommandName());
this.setResponseObject(response);
}
}

View File

@ -0,0 +1,60 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.gui.theme;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.GuiThemeResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.gui.theme.GuiTheme;
import org.apache.cloudstack.gui.theme.GuiThemeService;
import javax.inject.Inject;
@APICommand(name = "removeGuiTheme", description = "Removes an existing GUI theme.", responseObject = GuiThemeResponse.class, entityType = {GuiTheme.class},
since = "4.21.0.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin})
public class RemoveGuiThemeCmd extends BaseCmd {
@Inject
GuiThemeService guiThemeService;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GuiThemeResponse.class, required = true,
description = "The unique identifier of the GUI theme to be removed.")
private Long id;
public Long getId() {
return id;
}
@Override
public void execute() {
guiThemeService.removeGuiTheme(this);
final SuccessResponse response = new SuccessResponse();
response.setResponseName(getCommandName());
response.setSuccess(true);
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccountId();
}
}

View File

@ -0,0 +1,136 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.gui.theme;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.GuiThemeResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.gui.theme.GuiTheme;
import org.apache.cloudstack.gui.theme.GuiThemeJoin;
import org.apache.cloudstack.gui.theme.GuiThemeService;
import javax.inject.Inject;
@APICommand(name = "updateGuiTheme", description = "Updates an existing GUI theme.", responseObject = GuiThemeResponse.class, entityType = {GuiTheme.class},
since = "4.21.0.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin})
public class UpdateGuiThemeCmd extends BaseCmd {
@Inject
GuiThemeService guiThemeService;
@Parameter(name = ApiConstants.ID, required = true, type = CommandType.UUID, entityType = GuiThemeResponse.class, description = "The ID of the theme to be updated.")
private Long id;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, length = 2048, description = "A name to identify the theme.")
private String name;
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, length = 4096, description = "A description for the theme.")
private String description;
@Parameter(name = ApiConstants.CSS, type = CommandType.STRING, length = 65535, description = "The CSS to be retrieved and imported into the GUI " +
"when matching the theme access configurations.")
private String css;
@Parameter(name = ApiConstants.JSON_CONFIGURATION, type = CommandType.STRING, length = 65535, description = "The JSON with the configurations to be " +
"retrieved and imported into the GUI when matching the theme access configurations.")
private String jsonConfiguration;
@Parameter(name = ApiConstants.COMMON_NAMES, type = CommandType.STRING, length = 65535, description = "A set of Common Names (CN) (fixed or " +
"wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com")
private String commonNames;
@Parameter(name = ApiConstants.DOMAIN_IDS, type = CommandType.STRING, length = 65535, description = "A set of domain UUIDs (also known as ID for " +
"the end-user) separated by comma that can retrieve the theme.")
private String domainIds;
@Parameter(name = ApiConstants.RECURSIVE_DOMAINS, type = CommandType.BOOLEAN, description = "Defines whether the subdomains of the informed domains are considered. Default " +
"value is false.")
private Boolean recursiveDomains = false;
@Parameter(name = ApiConstants.ACCOUNT_IDS, type = CommandType.STRING, length = 65535, description = "A set of account UUIDs (also known as ID for" +
" the end-user) separated by comma that can retrieve the theme.")
private String accountIds;
@Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, description = "Defines whether a theme can be retrieved by anyone when only " +
"the `commonNames` is informed. If the `domainIds` or `accountIds` is informed, it is considered as `false`.")
private Boolean isPublic = true;
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getCss() {
return css;
}
public String getJsonConfiguration() {
return jsonConfiguration;
}
public String getCommonNames() {
return commonNames;
}
public String getDomainIds() {
return domainIds;
}
public String getAccountIds() {
return accountIds;
}
public Boolean getRecursiveDomains() {
return recursiveDomains;
}
public Boolean getIsPublic() {
return isPublic;
}
@Override
public void execute() {
GuiThemeJoin guiThemeJoin = guiThemeService.updateGuiTheme(this);
if (guiThemeJoin == null) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update the GUI theme.");
}
GuiThemeResponse response = _responseGenerator.createGuiThemeResponse(guiThemeJoin);
response.setResponseName(getCommandName());
this.setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccountId();
}
}

View File

@ -0,0 +1,179 @@
//Licensed to the Apache Software Foundation (ASF) under one
//or more contributor license agreements. See the NOTICE file
//distributed with this work for additional information
//regarding copyright ownership. The ASF licenses this file
//to you under the Apache License, Version 2.0 (the
//"License"); you may not use this file except in compliance
//with the License. You may obtain a copy of the License at
//
//http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing,
//software distributed under the License is distributed on an
//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
//KIND, either express or implied. See the License for the
//specific language governing permissions and limitations
//under the License.
package org.apache.cloudstack.api.response;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.gui.theme.GuiThemeJoin;
import java.util.Date;
@EntityReference(value = {GuiThemeJoin.class})
public class GuiThemeResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "ID of the custom GUI theme.")
private String id;
@SerializedName(ApiConstants.NAME)
@Param(description = "Name of the GUI theme.")
private String name;
@SerializedName(ApiConstants.DESCRIPTION)
@Param(description = "Description of the GUI theme.")
private String description;
@SerializedName(ApiConstants.CSS)
@Param(description = "The CSS to be retrieved and imported into the GUI when matching the theme access configurations.")
private String css;
@SerializedName(ApiConstants.JSON_CONFIGURATION)
@Param(description = "The JSON with the configurations to be retrieved and imported into the GUI when matching the theme access configurations.")
private String jsonConfiguration;
@SerializedName(ApiConstants.COMMON_NAMES)
@Param(description = "A set of Common Names (CN) (fixed or wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com")
private String commonNames;
@SerializedName(ApiConstants.DOMAIN_IDS)
@Param(description = "A set of domain UUIDs (also known as ID for the end-user) separated by comma that can retrieve the theme.")
private String domainIds;
@SerializedName(ApiConstants.RECURSIVE_DOMAINS)
@Param(description = "Whether to consider the subdomains of the informed domain IDs.")
private Boolean recursiveDomains;
@SerializedName(ApiConstants.ACCOUNT_IDS)
@Param(description = "A set of account UUIDs (also known as ID for the end-user) separated by comma that can retrieve the theme.")
private String accountIds;
@SerializedName(ApiConstants.IS_PUBLIC)
@Param(description = "Defines whether a theme can be retrieved by anyone when only the `commonNames` is informed. If the `domainIds` or `accountIds` is informed, it is " +
"considered as `false`.")
private Boolean isPublic;
@SerializedName(ApiConstants.CREATED)
@Param(description = "When the GUI theme was created.")
private Date created;
@SerializedName(ApiConstants.REMOVED)
@Param(description = "When the GUI theme was removed.")
private Date removed;
public GuiThemeResponse() {
super("guiThemes");
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCss() {
return css;
}
public void setCss(String css) {
this.css = css;
}
public String getJsonConfiguration() {
return jsonConfiguration;
}
public void setJsonConfiguration(String jsonConfiguration) {
this.jsonConfiguration = jsonConfiguration;
}
public String getCommonNames() {
return commonNames;
}
public void setCommonNames(String commonNames) {
this.commonNames = commonNames;
}
public String getDomainIds() {
return domainIds;
}
public void setDomainIds(String domainIds) {
this.domainIds = domainIds;
}
public String getAccountIds() {
return accountIds;
}
public void setAccountIds(String accountIds) {
this.accountIds = accountIds;
}
public Boolean getPublic() {
return isPublic;
}
public void setPublic(Boolean aPublic) {
isPublic = aPublic;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public Date getRemoved() {
return removed;
}
public Boolean getRecursiveDomains() {
return recursiveDomains;
}
public void setRecursiveDomains(Boolean recursiveDomains) {
this.recursiveDomains = recursiveDomains;
}
public void setRemoved(Date removed) {
this.removed = removed;
}
}

View File

@ -0,0 +1,61 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import java.util.Date;
public interface GuiTheme extends InternalIdentity, Identity {
String getName();
String getDescription();
String getCss();
String getJsonConfiguration();
Date getCreated();
Date getRemoved();
boolean getIsPublic();
void setId(Long id);
void setUuid(String uuid);
void setName(String name);
void setDescription(String description);
void setCss(String css);
void setJsonConfiguration(String jsonConfiguration);
void setCreated(Date created);
void setRemoved(Date removed);
void setIsPublic(boolean isPublic);
boolean isRecursiveDomains();
void setRecursiveDomains(boolean recursiveDomains);
}

View File

@ -0,0 +1,36 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme;
import org.apache.cloudstack.api.InternalIdentity;
public interface GuiThemeDetails extends InternalIdentity {
void setId(Long id);
Long getGuiThemeId();
void setGuiThemeId(Long guiThemeId);
String getType();
void setType(String type);
String getValue();
void setValue(String value);
}

View File

@ -0,0 +1,47 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import java.util.Date;
public interface GuiThemeJoin extends InternalIdentity, Identity {
String getName();
String getDescription();
String getCss();
String getJsonConfiguration();
String getCommonNames();
String getDomains();
String getAccounts();
boolean isRecursiveDomains();
boolean getIsPublic();
Date getCreated();
Date getRemoved();
}

View File

@ -0,0 +1,35 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme;
import org.apache.cloudstack.api.command.user.gui.theme.CreateGuiThemeCmd;
import org.apache.cloudstack.api.command.user.gui.theme.ListGuiThemesCmd;
import org.apache.cloudstack.api.command.user.gui.theme.RemoveGuiThemeCmd;
import org.apache.cloudstack.api.command.user.gui.theme.UpdateGuiThemeCmd;
import org.apache.cloudstack.api.response.GuiThemeResponse;
import org.apache.cloudstack.api.response.ListResponse;
public interface GuiThemeService {
ListResponse<GuiThemeResponse> listGuiThemes(ListGuiThemesCmd cmd);
GuiThemeJoin createGuiTheme(CreateGuiThemeCmd cmd);
GuiThemeJoin updateGuiTheme(UpdateGuiThemeCmd cmd);
void removeGuiTheme(RemoveGuiThemeCmd cmd);
}

View File

@ -0,0 +1,91 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "gui_themes_details")
public class GuiThemeDetailsVO implements GuiThemeDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "gui_theme_id", nullable = false)
private Long guiThemeId;
@Column(name = "type", nullable = false, length = 100)
private String type;
@Column(name = "value", nullable = false, length = 65535)
private String value;
public GuiThemeDetailsVO() {
}
public GuiThemeDetailsVO(Long guiThemeId, String type, String value) {
this.guiThemeId = guiThemeId;
this.type = type;
this.value = value;
}
@Override
public long getId() {
return id;
}
@Override
public void setId(Long id) {
this.id = id;
}
@Override
public Long getGuiThemeId() {
return guiThemeId;
}
@Override
public void setGuiThemeId(Long guiThemeId) {
this.guiThemeId = guiThemeId;
}
@Override
public String getType() {
return type;
}
@Override
public void setType(String type) {
this.type = type;
}
@Override
public String getValue() {
return value;
}
@Override
public void setValue(String value) {
this.value = value;
}
}

View File

@ -0,0 +1,141 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme;
import com.cloud.utils.db.GenericDao;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.util.Date;
@Entity
@Table(name = "gui_themes_view")
public class GuiThemeJoinVO implements GuiThemeJoin {
@Id
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "uuid", nullable = false)
private String uuid;
@Column(name = "name", nullable = false, length = 2048)
private String name;
@Column(name = "description", length = 4096)
private String description;
@Column(name = "css", nullable = false, length = 65535)
private String css;
@Column(name = "json_configuration", nullable = false, length = 65535)
private String jsonConfiguration;
@Column(name = "common_names", length = 65535)
private String commonNames;
@Column(name = "domains", length = 65535)
private String domains;
@Column(name = "accounts", length = 65535)
private String accounts;
@Column(name = "recursive_domains")
private boolean recursiveDomains;
@Column(name = "is_public")
private boolean isPublic;
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
@Temporal(value = TemporalType.TIMESTAMP)
private Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
@Temporal(value = TemporalType.TIMESTAMP)
private Date removed = null;
public GuiThemeJoinVO() {
}
@Override
public long getId() {
return id;
}
@Override
public String getUuid() {
return uuid;
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public String getCss() {
return css;
}
@Override
public String getJsonConfiguration() {
return jsonConfiguration;
}
@Override
public String getCommonNames() {
return commonNames;
}
@Override
public String getDomains() {
return domains;
}
@Override
public String getAccounts() {
return accounts;
}
@Override
public boolean isRecursiveDomains() {
return recursiveDomains;
}
@Override
public boolean getIsPublic() {
return isPublic;
}
@Override
public Date getCreated() {
return created;
}
@Override
public Date getRemoved() {
return removed;
}
}

View File

@ -0,0 +1,189 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.util.Date;
import java.util.UUID;
@Entity
@Table(name = "gui_themes")
public class GuiThemeVO implements GuiTheme {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "uuid", nullable = false)
private String uuid = UUID.randomUUID().toString();
@Column(name = "name", nullable = false, length = 2048)
private String name;
@Column(name = "description", length = 4096)
private String description;
@Column(name = "css", length = 65535)
private String css;
@Column(name = "json_configuration", length = 65535)
private String jsonConfiguration;
@Column(name = "is_public")
private boolean isPublic;
@Column(name = "recursive_domains")
private boolean recursiveDomains = false;
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
@Temporal(value = TemporalType.TIMESTAMP)
private Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
@Temporal(value = TemporalType.TIMESTAMP)
private Date removed = null;
public GuiThemeVO() {
}
public GuiThemeVO(String name, String description, String css, String jsonConfiguration, boolean recursiveDomains, boolean isPublic, Date created, Date removed) {
this.name = name;
this.description = description;
this.css = css;
this.jsonConfiguration = jsonConfiguration;
this.recursiveDomains = recursiveDomains;
this.isPublic = isPublic;
this.created = created;
this.removed = removed;
}
@Override
public long getId() {
return id;
}
@Override
public String getUuid() {
return uuid;
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public String getCss() {
return css;
}
@Override
public String getJsonConfiguration() {
return jsonConfiguration;
}
@Override
public Date getCreated() {
return created;
}
@Override
public Date getRemoved() {
return removed;
}
@Override
public boolean getIsPublic() {
return isPublic;
}
@Override
public void setId(Long id) {
this.id = id;
}
@Override
public void setUuid(String uuid) {
this.uuid = uuid;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setDescription(String description) {
this.description = description;
}
@Override
public void setCss(String css) {
this.css = css;
}
@Override
public void setJsonConfiguration(String jsonConfiguration) {
this.jsonConfiguration = jsonConfiguration;
}
@Override
public void setCreated(Date created) {
this.created = created;
}
@Override
public void setRemoved(Date removed) {
this.removed = removed;
}
@Override
public void setIsPublic(boolean isPublic) {
this.isPublic = isPublic;
}
@Override
public boolean isRecursiveDomains() {
return recursiveDomains;
}
@Override
public void setRecursiveDomains(boolean recursiveDomains) {
this.recursiveDomains = recursiveDomains;
}
@Override
public String toString() {
return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "uuid", "name", "description", "isPublic", "recursiveDomains");
}
}

View File

@ -0,0 +1,24 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.dao;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.gui.theme.GuiThemeVO;
public interface GuiThemeDao extends GenericDao<GuiThemeVO, Long> {
}

View File

@ -0,0 +1,25 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.dao;
import com.cloud.utils.db.GenericDaoBase;
import org.apache.cloudstack.gui.theme.GuiThemeVO;
import org.springframework.stereotype.Component;
@Component
public class GuiThemeDaoImpl extends GenericDaoBase<GuiThemeVO, Long> implements GuiThemeDao {
}

View File

@ -0,0 +1,30 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.dao;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.gui.theme.GuiThemeDetailsVO;
import java.util.List;
public interface GuiThemeDetailsDao extends GenericDao<GuiThemeDetailsVO, Long> {
List<Long> listGuiThemeIdsByCommonName(String commonName);
List<Long> listGuiThemeIdsByDomainUuids(String domainUuid);
void expungeByGuiThemeId(long guiThemeId);
}

View File

@ -0,0 +1,126 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.dao;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.GenericSearchBuilder;
import com.cloud.utils.db.JoinBuilder;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.gui.theme.GuiThemeDetailsVO;
import org.apache.cloudstack.gui.theme.GuiThemeVO;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@Component
public class GuiThemeDetailsDaoImpl extends GenericDaoBase<GuiThemeDetailsVO, Long> implements GuiThemeDetailsDao {
@Inject
DomainDao domainDao;
@Inject
GuiThemeDao guiThemeDao;
public List<Long> listGuiThemeIdsByCommonName(String commonName) {
GenericSearchBuilder<GuiThemeDetailsVO, Long> detailsDaoSearchBuilder = createSearchBuilder(Long.class);
detailsDaoSearchBuilder.selectFields(detailsDaoSearchBuilder.entity().getGuiThemeId());
detailsDaoSearchBuilder.and("commonNameType", detailsDaoSearchBuilder.entity().getType(), SearchCriteria.Op.EQ);
detailsDaoSearchBuilder.and().op("firstReplace", detailsDaoSearchBuilder.entity().getValue(), SearchCriteria.Op.LIKE_REPLACE);
detailsDaoSearchBuilder.or("secondReplace", detailsDaoSearchBuilder.entity().getValue(), SearchCriteria.Op.LIKE_REPLACE).cp();
detailsDaoSearchBuilder.done();
SearchCriteria<Long> searchCriteria = detailsDaoSearchBuilder.create();
searchCriteria.setParameters("commonNameType", "commonName");
searchCriteria.setParameters("firstReplace", commonName, "*", "%");
searchCriteria.setParameters("secondReplace", commonName, "*.", "%");
return customSearch(searchCriteria, null);
}
public List<Long> listGuiThemeIdsByDomainUuids(String domainUuid) {
List<Long> guiThemeIds = new ArrayList<>();
String requestedDomainPath = domainDao.findByUuid(domainUuid).getPath();
SearchBuilder<DomainVO> domainSearchBuilderPathLike = domainDao.createSearchBuilder();
domainSearchBuilderPathLike.and("pathLike", domainSearchBuilderPathLike.entity().getPath(), SearchCriteria.Op.LIKE_CONCAT);
SearchBuilder<DomainVO> domainSearchBuilderPathEq = domainDao.createSearchBuilder();
domainSearchBuilderPathEq.and("pathEq", domainSearchBuilderPathEq.entity().getPath(), SearchCriteria.Op.EQ);
GenericSearchBuilder<GuiThemeDetailsVO, Long> detailsSearchBuilderPathLike = createDetailsSearchBuilder(domainSearchBuilderPathLike);
SearchCriteria<Long> searchCriteriaDomainPathLike = setParametersDomainPathLike(detailsSearchBuilderPathLike, requestedDomainPath);
GenericSearchBuilder<GuiThemeDetailsVO, Long> detailsSearchBuilderPathEq = createDetailsSearchBuilder(domainSearchBuilderPathEq);
SearchCriteria<Long> searchCriteriaDomainPathEq = setParametersDomainPathEq(detailsSearchBuilderPathEq, requestedDomainPath);
guiThemeIds.addAll(customSearch(searchCriteriaDomainPathLike, null));
guiThemeIds.addAll(customSearch(searchCriteriaDomainPathEq, null));
return guiThemeIds;
}
private SearchCriteria<Long> setParametersDomainPathLike(GenericSearchBuilder<GuiThemeDetailsVO, Long> detailsSearchBuilderPathLike, String requestedDomainPath) {
SearchCriteria<Long> searchCriteria = detailsSearchBuilderPathLike.create();
searchCriteria.setParameters("domainUuidType", "domain");
searchCriteria.setJoinParameters("domainJoin", "pathLike", requestedDomainPath, "%");
searchCriteria.setJoinParameters("guiThemesJoin", "recursiveDomains", true);
return searchCriteria;
}
private SearchCriteria<Long> setParametersDomainPathEq(GenericSearchBuilder<GuiThemeDetailsVO, Long> detailsSearchBuilderPathEq, String requestedDomainPath) {
SearchCriteria<Long> searchCriteria = detailsSearchBuilderPathEq.create();
searchCriteria.setParameters("domainUuidType", "domain");
searchCriteria.setJoinParameters("domainJoin", "pathEq", requestedDomainPath);
searchCriteria.setJoinParameters("guiThemesJoin", "recursiveDomains", false);
return searchCriteria;
}
private GenericSearchBuilder<GuiThemeDetailsVO, Long> createDetailsSearchBuilder(SearchBuilder<DomainVO> domainSearchBuilder) {
SearchBuilder<GuiThemeVO> guiThemeDaoSearchBuilder = guiThemeDao.createSearchBuilder();
guiThemeDaoSearchBuilder.and("recursiveDomains", guiThemeDaoSearchBuilder.entity().isRecursiveDomains(), SearchCriteria.Op.EQ);
GenericSearchBuilder<GuiThemeDetailsVO, Long> guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder = createSearchBuilder(Long.class);
guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.selectFields(guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.entity().getGuiThemeId());
guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.and("domainUuidType", guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.entity().getType(), SearchCriteria.Op.EQ);
guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.join("domainJoin", domainSearchBuilder, domainSearchBuilder.entity().getUuid(),
guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.entity().getValue(), JoinBuilder.JoinType.INNER);
guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.join("guiThemesJoin", guiThemeDaoSearchBuilder, guiThemeDaoSearchBuilder.entity().getId(),
guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.entity().getGuiThemeId(), JoinBuilder.JoinType.INNER);
domainSearchBuilder.done();
guiThemeDaoSearchBuilder.done();
guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.done();
return guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder;
}
public void expungeByGuiThemeId(long guiThemeId) {
SearchBuilder<GuiThemeDetailsVO> searchBuilder = createSearchBuilder();
searchBuilder.and("guiThemeId", searchBuilder.entity().getGuiThemeId(), SearchCriteria.Op.EQ);
searchBuilder.done();
SearchCriteria<GuiThemeDetailsVO> searchCriteria = searchBuilder.create();
searchCriteria.setParameters("guiThemeId", guiThemeId);
expunge(searchCriteria);
}
}

View File

@ -0,0 +1,31 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.dao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.gui.theme.GuiThemeJoinVO;
import java.util.List;
public interface GuiThemeJoinDao extends GenericDao<GuiThemeJoinVO, Long> {
GuiThemeJoinVO findDefaultTheme();
Pair<List<GuiThemeJoinVO>, Integer> listGuiThemesWithNoAuthentication(String commonName);
Pair<List<GuiThemeJoinVO>, Integer> listGuiThemes(Long id, String name, String commonName, String domainUuid, String accountUuid, boolean listAll, boolean showRemoved, Boolean showPublic);
}

View File

@ -0,0 +1,139 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.dao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.gui.theme.GuiThemeJoinVO;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@Component
public class GuiThemeJoinDaoImpl extends GenericDaoBase<GuiThemeJoinVO, Long> implements GuiThemeJoinDao {
@Inject
GuiThemeDetailsDao guiThemeDetailsDao;
public static final Long INVALID_ID = -1L;
public GuiThemeJoinVO findDefaultTheme() {
SearchBuilder<GuiThemeJoinVO> searchBuilder = createSearchBuilder();
searchBuilder.and("commonNames", searchBuilder.entity().getCommonNames(), SearchCriteria.Op.NULL);
searchBuilder.and("domainUuids", searchBuilder.entity().getDomains(), SearchCriteria.Op.NULL);
searchBuilder.and("accountUuids", searchBuilder.entity().getAccounts(), SearchCriteria.Op.NULL);
searchBuilder.done();
SearchCriteria<GuiThemeJoinVO> searchCriteria = searchBuilder.create();
return findOneBy(searchCriteria);
}
public Pair<List<GuiThemeJoinVO>, Integer> listGuiThemesWithNoAuthentication(String commonName) {
SearchCriteria<GuiThemeJoinVO> searchCriteria = createGuiThemeSearchCriteria(null, null, commonName, null, null, null, false);
return searchOrderByCreatedDate(searchCriteria, false);
}
public Pair<List<GuiThemeJoinVO>, Integer> listGuiThemes(Long id, String name, String commonName, String domainUuid, String accountUuid, boolean listAll,
boolean showRemoved, Boolean showPublic) {
SearchCriteria<GuiThemeJoinVO> searchCriteria = createGuiThemeSearchCriteria(id, name, commonName, domainUuid, accountUuid, showPublic, listAll);
if (listAll) {
showRemoved = false;
}
return searchOrderByCreatedDate(searchCriteria, showRemoved);
}
private Pair<List<GuiThemeJoinVO>, Integer> searchOrderByCreatedDate(SearchCriteria<GuiThemeJoinVO> searchCriteria, boolean showRemoved) {
Filter filter = new Filter(GuiThemeJoinVO.class, "created", false);
return searchAndCount(searchCriteria, filter, showRemoved);
}
private SearchCriteria<GuiThemeJoinVO> createGuiThemeSearchCriteria(Long id, String name, String commonName, String domainUuid, String accountUuid, Boolean showPublic, boolean listAll) {
SearchCriteria<GuiThemeJoinVO> searchCriteria = createGuiThemeJoinSearchBuilder(listAll, showPublic).create();
List<Long> idList = new ArrayList<>();
if (id != null) {
idList.add(id);
}
searchCriteria.setParametersIfNotNull("name", name);
searchCriteria.setParametersIfNotNull("isPublic", showPublic);
if (StringUtils.isNotBlank(accountUuid)) {
searchCriteria.setParameters("accountUuid", "%" + accountUuid + "%");
}
if (StringUtils.isNotBlank(commonName)) {
setGuiThemeIdsFilteredByType(idList, ApiConstants.COMMON_NAME, commonName);
}
if (StringUtils.isNotBlank(domainUuid)) {
setGuiThemeIdsFilteredByType(idList, ApiConstants.DOMAIN, domainUuid);
}
searchCriteria.setParametersIfNotNull("idIn", idList.toArray());
return searchCriteria;
}
/**
* Sets the `id IN ( )` clause of the query. If the informed value of common name or domain ID does not retrieve any GUI theme ID; then, an invalid ID (-1) is passed to the
* list, as not a single entity has this ID. This is necessary as to set the parameter even if it did not find any GUI theme ID; otherwise, the query would not filter the
* common name or domain ID passed.
*/
public void setGuiThemeIdsFilteredByType(List<Long> idList, String type, String value) {
List<Long> guiThemeIdsFilteredByType = new ArrayList<>();
switch (type) {
case ApiConstants.COMMON_NAME:
guiThemeIdsFilteredByType = guiThemeDetailsDao.listGuiThemeIdsByCommonName(value);
break;
case ApiConstants.DOMAIN:
guiThemeIdsFilteredByType = guiThemeDetailsDao.listGuiThemeIdsByDomainUuids(value);
break;
}
if (CollectionUtils.isNotEmpty(guiThemeIdsFilteredByType)) {
idList.addAll(guiThemeIdsFilteredByType);
return;
}
logger.trace(String.format("No GUI theme with the specified [%s] with UUID [%s] was found, adding an invalid ID for filtering.", type, value));
idList.add(INVALID_ID);
}
private SearchBuilder<GuiThemeJoinVO> createGuiThemeJoinSearchBuilder(boolean listAll, Boolean showPublic) {
SearchBuilder<GuiThemeJoinVO> guiThemeJoinSearchBuilder = createSearchBuilder();
guiThemeJoinSearchBuilder.and("idIn", guiThemeJoinSearchBuilder.entity().getId(), SearchCriteria.Op.IN);
guiThemeJoinSearchBuilder.and("name", guiThemeJoinSearchBuilder.entity().getName(), SearchCriteria.Op.EQ);
guiThemeJoinSearchBuilder.and("accountUuid", guiThemeJoinSearchBuilder.entity().getAccounts(), SearchCriteria.Op.LIKE);
if (!listAll && showPublic != null) {
guiThemeJoinSearchBuilder.and("isPublic", guiThemeJoinSearchBuilder.entity().getIsPublic(), SearchCriteria.Op.EQ);
}
return guiThemeJoinSearchBuilder;
}
}

View File

@ -301,4 +301,7 @@
<bean id="SharedFSDaoImpl" class="org.apache.cloudstack.storage.sharedfs.dao.SharedFSDaoImpl" />
<bean id="SharedFSJoinDaoImpl" class="org.apache.cloudstack.storage.sharedfs.query.dao.SharedFSJoinDaoImpl" />
<bean id="ReconcileCommandDaoImpl" class="org.apache.cloudstack.command.dao.ReconcileCommandDaoImpl" />
<bean id="guiThemeDaoImpl" class="org.apache.cloudstack.gui.theme.dao.GuiThemeDaoImpl" />
<bean id="guiThemeDetailsDaoImpl" class="org.apache.cloudstack.gui.theme.dao.GuiThemeDetailsDaoImpl" />
<bean id="guiThemeJoinDaoImpl" class="org.apache.cloudstack.gui.theme.dao.GuiThemeJoinDaoImpl" />
</beans>

View File

@ -203,3 +203,27 @@ SET `sort_key` = CASE
ELSE `sort_key`
END;
-- End: Changes for Guest OS category cleanup
-- Whitelabel GUI
CREATE TABLE IF NOT EXISTS `cloud`.`gui_themes` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`uuid` varchar(255) UNIQUE,
`name` varchar(2048) NOT NULL COMMENT 'A name to identify the theme.',
`description` varchar(4096) DEFAULT NULL COMMENT 'A description for the theme.',
`css` text DEFAULT NULL COMMENT 'The CSS to be retrieved and imported into the GUI when matching the theme access configurations.',
`json_configuration` text DEFAULT NULL COMMENT 'The JSON with the configurations to be retrieved and imported into the GUI when matching the theme access configurations.',
`recursive_domains` tinyint(1) DEFAULT 0 COMMENT 'Defines whether the subdomains of the informed domains are considered. Default value is false.',
`is_public` tinyint(1) default 1 COMMENT 'Defines whether a theme can be retrieved by anyone when only the `internet_domains_names` is informed. If the `domain_uuids` or `account_uuids` is informed, it is considered as `false`.',
`created` datetime NOT NULL,
`removed` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS `cloud`.`gui_themes_details` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`gui_theme_id` bigint(20) unsigned NOT NULL COMMENT 'Foreign key referencing the GUI theme on `gui_themes` table.',
`type` varchar(100) NOT NULL COMMENT 'The type of GUI theme details. Valid options are: `account`, `domain` and `commonName`',
`value` text NOT NULL COMMENT 'The value of the `type` details. Can be an UUID (account or domain) or internet common name.',
PRIMARY KEY (`id`),
CONSTRAINT `fk_gui_themes_details__gui_theme_id` FOREIGN KEY (`gui_theme_id`) REFERENCES `gui_themes`(`id`)
);

View File

@ -0,0 +1,38 @@
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing,
-- software distributed under the License is distributed on an
-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-- KIND, either express or implied. See the License for the
-- specific language governing permissions and limitations
-- under the License.
-- VIEW `cloud`.`gui_themes_view`;
DROP VIEW IF EXISTS `cloud`.`gui_themes_view`;
CREATE VIEW `cloud`.`gui_themes_view` AS
SELECT
`cloud`.`gui_themes`.`id` AS `id`,
`cloud`.`gui_themes`.`uuid` AS `uuid`,
`cloud`.`gui_themes`.`name` AS `name`,
`cloud`.`gui_themes`.`description` AS `description`,
`cloud`.`gui_themes`.`css` AS `css`,
`cloud`.`gui_themes`.`json_configuration` AS `json_configuration`,
(SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'commonName' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) common_names,
(SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'domain' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) domains,
(SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'account' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) accounts,
`cloud`.`gui_themes`.`recursive_domains` AS `recursive_domains`,
`cloud`.`gui_themes`.`is_public` AS `is_public`,
`cloud`.`gui_themes`.`created` AS `created`,
`cloud`.`gui_themes`.`removed` AS `removed`
FROM `cloud`.`gui_themes` LEFT JOIN `cloud`.`gui_themes_details` ON `cloud`.`gui_themes_details`.`gui_theme_id` = `cloud`.`gui_themes`.`id`
GROUP BY `cloud`.`gui_themes`.`id`;

View File

@ -479,6 +479,14 @@ public abstract class SearchBase<J extends SearchBase<?, T, K>, T, K> {
sql.append(" FIND_IN_SET(?, ");
}
if (op == Op.LIKE_REPLACE) {
sql.append(" ? LIKE REPLACE (");
}
if (op == Op.LIKE_CONCAT) {
sql.append(" ? LIKE CONCAT (");
}
if (tableAlias == null) {
if (joinName != null) {
tableAlias = joinName;

View File

@ -39,7 +39,8 @@ public class SearchCriteria<K> {
" NOT BETWEEN ? AND ? ",
2), IN(" IN () ", -1), NOTIN(" NOT IN () ", -1), LIKE(" LIKE ? ", 1), NLIKE(" NOT LIKE ? ", 1), NIN(" NOT IN () ", -1), NULL(" IS NULL ", 0), NNULL(
" IS NOT NULL ",
0), SC(" () ", 1), TEXT(" () ", 1), RP("", 0), AND(" AND ", 0), OR(" OR ", 0), NOT(" NOT ", 0), FIND_IN_SET(" ) ", 1), BINARY_OR(" & ?) > 0", 1);
0), SC(" () ", 1), TEXT(" () ", 1), RP("", 0), AND(" AND ", 0), OR(" OR ", 0), NOT(" NOT ", 0), FIND_IN_SET(" ) ", 1), BINARY_OR(" & ?) > 0", 1),
LIKE_REPLACE(", ?, ?)", 3), LIKE_CONCAT(", ?)", 2);
private final String op;
int params;
@ -60,7 +61,8 @@ public class SearchCriteria<K> {
}
public enum Func {
NATIVE("@", 1), MAX("MAX(@)", 1), MIN("MIN(@)", 1), FIRST("FIRST(@)", 1), LAST("LAST(@)", 1), SUM("SUM(@)", 1), COUNT("COUNT(@)", 1), DISTINCT("DISTINCT(@)", 1), DISTINCT_PAIR("DISTINCT @, @", 2);
NATIVE("@", 1), MAX("MAX(@)", 1), MIN("MIN(@)", 1), FIRST("FIRST(@)", 1), LAST("LAST(@)", 1), SUM("SUM(@)", 1), COUNT("COUNT(@)", 1), DISTINCT("DISTINCT(@)", 1), DISTINCT_PAIR("DISTINCT @, @", 2),
CONCAT("CONCAT(@,@)", 2);
private String func;
private int count;

View File

@ -97,6 +97,7 @@ import org.apache.cloudstack.api.response.GuestOSResponse;
import org.apache.cloudstack.api.response.GuestOsMappingResponse;
import org.apache.cloudstack.api.response.GuestVlanRangeResponse;
import org.apache.cloudstack.api.response.GuestVlanResponse;
import org.apache.cloudstack.api.response.GuiThemeResponse;
import org.apache.cloudstack.api.response.HostForMigrationResponse;
import org.apache.cloudstack.api.response.HostResponse;
import org.apache.cloudstack.api.response.HypervisorCapabilitiesResponse;
@ -206,6 +207,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.framework.jobs.AsyncJob;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
import org.apache.cloudstack.gui.theme.GuiThemeJoin;
import org.apache.cloudstack.management.ManagementServerHost;
import org.apache.cloudstack.network.BgpPeerVO;
import org.apache.cloudstack.network.RoutedIpv4Manager;
@ -5515,8 +5517,7 @@ public class ApiResponseHelper implements ResponseGenerator {
SharedFSJoinVO sharedFSView = ApiDBUtils.newSharedFSView(sharedFS);
return ApiDBUtils.newSharedFSResponse(view, sharedFSView);
}
protected Map<String, ResourceIcon> getResourceIconsUsingOsCategory(List<TemplateResponse> responses) {
protected Map<String, ResourceIcon> getResourceIconsUsingOsCategory(List<TemplateResponse> responses) {
Set<Long> guestOsCategoryIds = responses.stream().map(TemplateResponse::getOsTypeCategoryId).collect(Collectors.toSet());
Map<Long, ResourceIcon> guestOsCategoryIcons =
resourceIconManager.getByResourceTypeAndIds(ResourceTag.ResourceObjectType.GuestOsCategory,
@ -5550,4 +5551,28 @@ public class ApiResponseHelper implements ResponseGenerator {
response.setResourceIconResponse(iconResponse);
}
}
@Override
public GuiThemeResponse createGuiThemeResponse(GuiThemeJoin guiThemeJoin) {
GuiThemeResponse guiThemeResponse = new GuiThemeResponse();
Long callerId = CallContext.current().getCallingAccount().getAccountId();
if (callerId != Account.ACCOUNT_ID_SYSTEM && _accountMgr.isRootAdmin(callerId)) {
guiThemeResponse.setId(guiThemeJoin.getUuid());
guiThemeResponse.setName(guiThemeJoin.getName());
guiThemeResponse.setDescription(guiThemeJoin.getDescription());
guiThemeResponse.setCommonNames(guiThemeJoin.getCommonNames());
guiThemeResponse.setDomainIds(guiThemeJoin.getDomains());
guiThemeResponse.setRecursiveDomains(guiThemeJoin.isRecursiveDomains());
guiThemeResponse.setAccountIds(guiThemeJoin.getAccounts());
guiThemeResponse.setPublic(guiThemeJoin.getIsPublic());
guiThemeResponse.setCreated(guiThemeJoin.getCreated());
guiThemeResponse.setRemoved(guiThemeJoin.getRemoved());
}
guiThemeResponse.setJsonConfiguration(guiThemeJoin.getJsonConfiguration());
guiThemeResponse.setCss(guiThemeJoin.getCss());
guiThemeResponse.setResponseName("guithemes");
return guiThemeResponse;
}
}

View File

@ -87,6 +87,7 @@ import org.apache.cloudstack.api.command.admin.user.ListUsersCmd;
import org.apache.cloudstack.api.command.user.account.ListAccountsCmd;
import org.apache.cloudstack.api.command.user.account.ListProjectAccountsCmd;
import org.apache.cloudstack.api.command.user.event.ListEventsCmd;
import org.apache.cloudstack.api.command.user.gui.theme.ListGuiThemesCmd;
import org.apache.cloudstack.api.command.user.offering.ListDiskOfferingsCmd;
import org.apache.cloudstack.api.command.user.offering.ListServiceOfferingsCmd;
import org.apache.cloudstack.api.command.user.project.ListProjectInvitationsCmd;
@ -967,6 +968,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
final User user = ApiDBUtils.findUserById(userId);
return commandAvailable(remoteAddress, commandName, user);
} else {
if (commandName.equalsIgnoreCase(ListGuiThemesCmd.class.getAnnotation(APICommand.class).name())) {
return true;
}
// check against every available command to see if the command exists or not
if (!s_apiNameCmdClassMap.containsKey(commandName) && !commandName.equals("login") && !commandName.equals("logout")) {
final String errorMessage = "The given command " + commandName + " either does not exist, is not available" +

View File

@ -37,6 +37,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ApiServerService;
@ -46,6 +47,7 @@ import org.apache.cloudstack.api.auth.APIAuthenticationManager;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.command.user.consoleproxy.CreateConsoleEndpointCmd;
import org.apache.cloudstack.api.command.user.gui.theme.ListGuiThemesCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.managed.context.ManagedContext;
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
@ -397,6 +399,8 @@ public class ApiServlet extends HttpServlet {
CallContext.register(accountMgr.getSystemUser(), accountMgr.getSystemAccount());
}
setProjectContext(params);
setGuiThemeParameterIfApiCallIsUnauthenticated(userId, command, req, params);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("verifying request for user %s from %s with %d parameters",
userId, remoteAddress.getHostAddress(), params.size()));
@ -457,6 +461,19 @@ public class ApiServlet extends HttpServlet {
}
}
private void setGuiThemeParameterIfApiCallIsUnauthenticated(Long userId, String command, HttpServletRequest req, Map<String, Object[]> params) {
String listGuiThemesApiName = ListGuiThemesCmd.class.getAnnotation(APICommand.class).name();
if (userId != null || !listGuiThemesApiName.equalsIgnoreCase(command)) {
return;
}
String serverName = req.getServerName();
LOGGER.info("Unauthenticated call to {} API, thus, the `commonName` parameter will be inferred as {}.", listGuiThemesApiName, serverName);
params.put(ApiConstants.COMMON_NAME, new String[]{serverName});
}
private boolean checkIfAuthenticatorIsOf2FA(String command) {
boolean verify2FA = false;
APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command);

View File

@ -403,6 +403,10 @@ import org.apache.cloudstack.api.command.user.firewall.UpdateFirewallRuleCmd;
import org.apache.cloudstack.api.command.user.firewall.UpdatePortForwardingRuleCmd;
import org.apache.cloudstack.api.command.user.guest.ListGuestOsCategoriesCmd;
import org.apache.cloudstack.api.command.user.guest.ListGuestOsCmd;
import org.apache.cloudstack.api.command.user.gui.theme.CreateGuiThemeCmd;
import org.apache.cloudstack.api.command.user.gui.theme.ListGuiThemesCmd;
import org.apache.cloudstack.api.command.user.gui.theme.RemoveGuiThemeCmd;
import org.apache.cloudstack.api.command.user.gui.theme.UpdateGuiThemeCmd;
import org.apache.cloudstack.api.command.user.iso.AttachIsoCmd;
import org.apache.cloudstack.api.command.user.iso.CopyIsoCmd;
import org.apache.cloudstack.api.command.user.iso.DeleteIsoCmd;
@ -4187,6 +4191,10 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(UpdateSecondaryStorageSelectorCmd.class);
cmdList.add(RemoveSecondaryStorageSelectorCmd.class);
cmdList.add(ListAffectedVmsForStorageScopeChangeCmd.class);
cmdList.add(ListGuiThemesCmd.class);
cmdList.add(UpdateGuiThemeCmd.class);
cmdList.add(CreateGuiThemeCmd.class);
cmdList.add(RemoveGuiThemeCmd.class);
// Out-of-band management APIs for admins
cmdList.add(EnableOutOfBandManagementForHostCmd.class);

View File

@ -0,0 +1,445 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme;
import com.cloud.domain.Domain;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.user.Account;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.exception.CloudRuntimeException;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.command.user.gui.theme.CreateGuiThemeCmd;
import org.apache.cloudstack.api.command.user.gui.theme.ListGuiThemesCmd;
import org.apache.cloudstack.api.command.user.gui.theme.RemoveGuiThemeCmd;
import org.apache.cloudstack.api.command.user.gui.theme.UpdateGuiThemeCmd;
import org.apache.cloudstack.api.response.GuiThemeResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.gui.theme.dao.GuiThemeDao;
import org.apache.cloudstack.gui.theme.dao.GuiThemeDetailsDao;
import org.apache.cloudstack.gui.theme.dao.GuiThemeJoinDao;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
public class GuiThemeServiceImpl implements GuiThemeService {
protected Logger logger = LogManager.getLogger(getClass());
private static final List<String> ALLOWED_PRIMITIVE_PROPERTIES = List.of("appTitle", "footer", "loginFooter", "logo", "minilogo", "banner");
private static final List<String> ALLOWED_ERROR_PROPERTIES = List.of("403", "404", "500");
private static final List<String> ALLOWED_PLUGIN_PROPERTIES = List.of("name", "path", "icon", "isExternalLink");
private static final String ERROR = "error";
private static final String PLUGINS = "plugins";
@Inject
GuiThemeDao guiThemeDao;
@Inject
GuiThemeDetailsDao guiThemeDetailsDao;
@Inject
GuiThemeJoinDao guiThemeJoinDao;
@Inject
ResponseGenerator responseGenerator;
@Inject
EntityManager entityManager;
@Inject
AccountDao accountDao;
@Inject
DomainDao domainDao;
@Override
public ListResponse<GuiThemeResponse> listGuiThemes(ListGuiThemesCmd cmd) {
ListResponse<GuiThemeResponse> response = new ListResponse<>();
Pair<List<GuiThemeJoinVO>, Integer> result;
boolean listOnlyDefaultTheme = cmd.getListOnlyDefaultTheme();
if (listOnlyDefaultTheme) {
result = retrieveDefaultTheme();
} else if (CallContext.current().getCallingAccountId() == Account.ACCOUNT_ID_SYSTEM) {
logger.info("Unauthenticated call to `listGuiThemes` API, ignoring all parameters, except `commonName`.");
result = listGuiThemesWithNoAuthentication(cmd);
} else {
result = listGuiThemesInternal(cmd);
}
List<GuiThemeResponse> guiThemeResponses = new ArrayList<>();
for (GuiThemeJoin guiThemeJoin : result.first()) {
GuiThemeResponse guiThemeResponse = responseGenerator.createGuiThemeResponse(guiThemeJoin);
guiThemeResponses.add(guiThemeResponse);
}
response.setResponses(guiThemeResponses);
return response;
}
private Pair<List<GuiThemeJoinVO>, Integer> retrieveDefaultTheme() {
GuiThemeJoinVO defaultTheme = guiThemeJoinDao.findDefaultTheme();
List<GuiThemeJoinVO> list = new ArrayList<>();
if (defaultTheme != null) {
list.add(defaultTheme);
}
return new Pair<>(list, list.size());
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_GUI_THEME_CREATE, eventDescription = "Creating GUI theme")
public GuiThemeJoin createGuiTheme(CreateGuiThemeCmd cmd) {
String name = cmd.getName();
String description = cmd.getDescription();
String css = cmd.getCss();
String jsonConfiguration = cmd.getJsonConfiguration();
String commonNames = cmd.getCommonNames();
String providedDomainIds = cmd.getDomainIds();
String providedAccountIds = cmd.getAccountIds();
boolean isPublic = cmd.getPublic();
Boolean recursiveDomains = cmd.getRecursiveDomains();
CallContext.current().setEventDetails(String.format("Name: %s, AccountIDs: %s, DomainIDs: %s, RecursiveDomains: %s, CommonNames: %s", name, providedAccountIds,
providedDomainIds, recursiveDomains, commonNames));
if (StringUtils.isAllBlank(css, jsonConfiguration)) {
throw new CloudRuntimeException("Either the `css` or `jsonConfiguration` parameter must be informed.");
}
validateParameters(jsonConfiguration, providedDomainIds, providedAccountIds, commonNames, null);
if (shouldSetGuiThemeToPrivate(providedDomainIds, providedAccountIds)) {
isPublic = false;
}
GuiThemeVO guiThemeVO = new GuiThemeVO(name, description, css, jsonConfiguration, recursiveDomains, isPublic, new Date(), null);
guiThemeDao.persist(guiThemeVO);
persistGuiThemeDetails(guiThemeVO.getId(), commonNames, providedDomainIds, providedAccountIds);
return guiThemeJoinDao.findById(guiThemeVO.getId());
}
protected void persistGuiThemeDetails(long guiThemeId, String commonNames, String providedDomainIds, String providedAccountIds) {
persistDetailValueIfNotNull(guiThemeId, commonNames, "commonName");
persistDetailValueIfNotNull(guiThemeId, providedDomainIds, "domain");
persistDetailValueIfNotNull(guiThemeId, providedAccountIds, "account");
}
protected void persistDetailValueIfNotNull(long guiThemeId, String providedParameter, String type) {
if (providedParameter == null) {
logger.trace("GUI theme provided parameter `{}` is null; therefore, it will be ignored.", type);
return;
}
for (String splitParameter : StringUtils.deleteWhitespace(providedParameter).split(",")) {
guiThemeDetailsDao.persist(new GuiThemeDetailsVO(guiThemeId, type, splitParameter));
}
}
protected boolean shouldSetGuiThemeToPrivate(String providedDomainIds, String providedAccountIds) {
if (StringUtils.isNotBlank(providedAccountIds)) {
logger.info("Parameter `accountIds` was informed during GUI theme creation, therefore, `isPublic` will be set to `false`.");
return true;
}
if (StringUtils.isNotBlank(providedDomainIds)) {
logger.info("Parameter `domainIds` was informed during GUI theme creation, therefore, `isPublic` will be set to `false`.");
return true;
}
return false;
}
/**
* A GUI theme is only considered the default one if the parameters `commonNames`, `domainIds` and `accountIds` are all blank.
* @return true if all parameters are blank, false otherwise.
*/
protected boolean isConsideredDefaultTheme(String commonNames, String providedDomainIds, String providedAccountIds) {
return StringUtils.isAllBlank(commonNames, providedDomainIds, providedAccountIds);
}
/**
* There can only be one default theme registered, therefore, a {@link CloudRuntimeException} will be thrown if:
* <ul>
* <li>There is already a default theme registered when creating a new GUI theme.</li>
* <li>Or, the GUI theme to be updated is not the default theme already registered.</li>
* </ul>
*/
protected void checkIfDefaultThemeIsAllowed(String commonNames, String providedDomainIds, String providedAccountIds, Long idOfThemeToBeUpdated) {
if (!isConsideredDefaultTheme(commonNames, providedDomainIds, providedAccountIds)) {
logger.info("The GUI theme will not be considered as the default one, as the `commonNames`, `domainIds` and `accountIds` are not all blank.");
return;
}
GuiThemeJoinVO defaultTheme = guiThemeJoinDao.findDefaultTheme();
if (defaultTheme != null && (idOfThemeToBeUpdated == null || defaultTheme.getId() != idOfThemeToBeUpdated)) {
throw new CloudRuntimeException(String.format("Only one default GUI theme is allowed. Remove the current default theme %s and try again.", defaultTheme));
}
logger.info("The parameters `commonNames`, `domainIds` and `accountIds` were not informed. The created theme will be considered as the default theme.");
}
protected Pair<List<GuiThemeJoinVO>, Integer> listGuiThemesWithNoAuthentication(ListGuiThemesCmd cmd) {
return guiThemeJoinDao.listGuiThemesWithNoAuthentication(cmd.getCommonName());
}
protected Pair<List<GuiThemeJoinVO>, Integer> listGuiThemesInternal(ListGuiThemesCmd cmd) {
Long id = cmd.getId();
String name = cmd.getName();
String commonName = cmd.getCommonName();
String domainUuid = cmd.getDomainId() == null ? null : domainDao.findById(cmd.getDomainId()).getUuid();
String accountUuid = cmd.getAccountId() == null ? null : accountDao.findById(cmd.getAccountId()).getUuid();
boolean listAll = cmd.getListAll();
boolean showRemoved = cmd.getShowRemoved();
Boolean showPublic = cmd.getShowPublic();
return guiThemeJoinDao.listGuiThemes(id, name, commonName, domainUuid, accountUuid, listAll, showRemoved, showPublic);
}
protected void validateParameters(String jsonConfig, String domainIds, String accountIds, String commonNames, Long idOfThemeToBeUpdated) {
if (isConsideredDefaultTheme(commonNames, domainIds, accountIds)) {
checkIfDefaultThemeIsAllowed(commonNames, domainIds, accountIds, idOfThemeToBeUpdated);
}
validateObjectUuids(accountIds, Account.class);
validateObjectUuids(domainIds, Domain.class);
validateJsonConfiguration(jsonConfig);
}
protected void validateJsonConfiguration(String jsonConfig) {
if (jsonConfig == null) {
return;
}
JsonObject jsonObject = new JsonObject();
try {
JsonElement jsonElement = new JsonParser().parse(jsonConfig);
Set<Map.Entry<String, JsonElement>> entries = jsonElement.getAsJsonObject().entrySet();
entries.stream().forEach(entry -> validateJsonAttributes(entry, jsonObject));
} catch (JsonSyntaxException exception) {
logger.error("The following exception was thrown while parsing the JSON object: [{}].", exception.getMessage());
throw new CloudRuntimeException("Specified JSON configuration is not a valid JSON object.");
}
}
/**
* Validates the informed JSON attributes considering the allowed properties by the API, any invalid option is ignored.
* All valid options are added to a {@link JsonObject} that will be considered as the final JSON configuration used by the GUI theme.
*/
private void validateJsonAttributes(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
JsonElement entryValue = entry.getValue();
String entryKey = entry.getKey();
if (entryValue.isJsonPrimitive() && ALLOWED_PRIMITIVE_PROPERTIES.contains(entryKey)) {
logger.trace("The JSON attribute [{}] is a valid option.", entryKey);
jsonObject.add(entryKey, entryValue);
} else if (entryValue.isJsonObject() && ERROR.equals(entryKey)) {
validateErrorAttribute(entry, jsonObject);
} else if (entryValue.isJsonArray() && PLUGINS.equals(entryKey)) {
validatePluginsAttribute(entry, jsonObject);
} else {
warnOfInvalidJsonAttribute(entryKey);
}
}
/**
* Creates a {@link JsonObject} with only the valid options for the Plugins' properties specified in the {@link #ALLOWED_PLUGIN_PROPERTIES}.
*/
protected void validatePluginsAttribute(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
Set<Map.Entry<String, JsonElement>> entries = entry.getValue().getAsJsonArray().get(0).getAsJsonObject().entrySet();
JsonObject objectToBeAdded = createJsonObject(entries, ALLOWED_PLUGIN_PROPERTIES);
JsonArray jsonArray = new JsonArray();
if (objectToBeAdded.entrySet().isEmpty()) {
return;
}
jsonArray.add(objectToBeAdded);
jsonObject.add(entry.getKey(), jsonArray);
}
/**
* Creates a {@link JsonObject} with only the valid options for the Error's properties specified in the {@link #ALLOWED_ERROR_PROPERTIES}.
*/
protected void validateErrorAttribute(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
Set<Map.Entry<String, JsonElement>> entries = entry.getValue().getAsJsonObject().entrySet();
JsonObject objectToBeAdded = createJsonObject(entries, ALLOWED_ERROR_PROPERTIES);
if (objectToBeAdded.entrySet().isEmpty()) {
return;
}
jsonObject.add(entry.getKey(), objectToBeAdded);
}
protected JsonObject createJsonObject(Set<Map.Entry<String, JsonElement>> entries, List<String> allowedProperties) {
JsonObject objectToBeAdded = new JsonObject();
for (Map.Entry<String, JsonElement> recursiveEntry : entries) {
String entryKey = recursiveEntry.getKey();
if (!allowedProperties.contains(entryKey)) {
warnOfInvalidJsonAttribute(entryKey);
continue;
}
objectToBeAdded.add(entryKey, recursiveEntry.getValue());
}
return objectToBeAdded;
}
protected void warnOfInvalidJsonAttribute(String entryKey) {
logger.warn("The JSON attribute [{}] is not a valid option, therefore, it will be ignored.", entryKey);
}
/**
* Validate if the comma separated list of UUIDs of the fields {@link GuiThemeJoinVO#getAccounts()} and {@link GuiThemeJoinVO#getDomains()} are valid.
* @param providedIds a comma separated list of UUIDs of {@link Account} or {@link Domain}
* @param clazz the class to infer the DAO object. Valid options are: {@link Account} and {@link Domain}
*/
protected void validateObjectUuids(String providedIds, Class clazz) {
if (StringUtils.isBlank(providedIds)) {
return;
}
String[] commaSeparatedIds = providedIds.split("\\s*,\\s*");
for (String id : commaSeparatedIds) {
Object objectVO = entityManager.findByUuid(clazz, id);
if (objectVO == null) {
throw new CloudRuntimeException(String.format("The %s ID %s does not exist. Verify the informed IDs and try again.", clazz.getSimpleName(), id));
}
}
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_GUI_THEME_UPDATE, eventDescription = "Updating GUI theme")
public GuiThemeJoin updateGuiTheme(UpdateGuiThemeCmd cmd) {
Long guiThemeId = cmd.getId();
GuiThemeJoinVO guiThemeJoinVO = guiThemeJoinDao.findById(guiThemeId);
String name = cmd.getName();
String description = cmd.getDescription();
String css = cmd.getCss();
String jsonConfiguration = cmd.getJsonConfiguration();
String commonNames = cmd.getCommonNames() == null ? guiThemeJoinVO.getCommonNames() : cmd.getCommonNames();
String providedDomainIds = cmd.getDomainIds() == null ? guiThemeJoinVO.getDomains() : cmd.getDomainIds();
String providedAccountIds = cmd.getAccountIds() == null ? guiThemeJoinVO.getAccounts() : cmd.getAccountIds();
Boolean isPublic = cmd.getIsPublic();
Boolean recursiveDomains = cmd.getRecursiveDomains();
CallContext.current().setEventDetails(String.format("ID: %s, Name: %s, AccountIDs: %s, DomainIDs: %s, RecursiveDomains: %s, CommonNames: %s", guiThemeId, name,
providedAccountIds, providedDomainIds, recursiveDomains, commonNames));
validateParameters(jsonConfiguration, providedDomainIds, providedAccountIds, commonNames, guiThemeId);
if (shouldSetGuiThemeToPrivate(providedDomainIds, providedAccountIds)) {
isPublic = false;
}
return persistGuiTheme(guiThemeId, name, description, css, jsonConfiguration, commonNames, providedDomainIds, providedAccountIds, isPublic, recursiveDomains);
}
protected GuiThemeJoinVO persistGuiTheme(Long guiThemeId, String name, String description, String css, String jsonConfiguration, String commonNames, String providedDomainIds,
String providedAccountIds, Boolean isPublic, Boolean recursiveDomains){
return Transaction.execute((TransactionCallback<GuiThemeJoinVO>) status -> {
GuiThemeVO guiThemeVO = guiThemeDao.findById(guiThemeId);
if (name != null) {
guiThemeVO.setName(ifBlankReturnNull(name));
}
if (description != null) {
guiThemeVO.setDescription(ifBlankReturnNull(description));
}
if (css != null) {
guiThemeVO.setCss(css);
}
if (jsonConfiguration != null) {
guiThemeVO.setJsonConfiguration(jsonConfiguration);
}
if (isPublic != null) {
guiThemeVO.setIsPublic(isPublic);
}
if (recursiveDomains != null) {
guiThemeVO.setRecursiveDomains(recursiveDomains);
}
logger.trace("Persisting GUI theme [{}] with CSS [{}] and JSON configuration [{}].", guiThemeVO, guiThemeVO.getCss(), guiThemeVO.getJsonConfiguration());
guiThemeDao.persist(guiThemeVO);
guiThemeDetailsDao.expungeByGuiThemeId(guiThemeId);
persistGuiThemeDetails(guiThemeId, commonNames, providedDomainIds, providedAccountIds);
return guiThemeJoinDao.findById(guiThemeId);
});
}
protected String ifBlankReturnNull(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
return value;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_GUI_THEME_REMOVE, eventDescription = "Removing GUI theme")
public void removeGuiTheme(RemoveGuiThemeCmd cmd) {
Long guiThemeId = cmd.getId();
GuiThemeVO guiThemeVO = guiThemeDao.findById(guiThemeId);
CallContext.current().setEventDetails(String.format("ID: %s", guiThemeId));
if (guiThemeVO != null) {
guiThemeDao.remove(guiThemeId);
} else {
logger.error("Unable to find a GUI theme with the specified ID [{}].", guiThemeId);
throw new CloudRuntimeException("Unable to find a GUI theme with the specified ID.");
}
}
}

View File

@ -390,4 +390,5 @@
<property name="asyncJobDispatcher" ref="ApiAsyncJobDispatcher" />
</bean>
<bean id="guiThemeServiceImpl" class="org.apache.cloudstack.gui.theme.GuiThemeServiceImpl" />
</beans>

View File

@ -0,0 +1,215 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme;
import com.cloud.user.Account;
import com.cloud.utils.Pair;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.api.command.user.gui.theme.ListGuiThemesCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.gui.theme.dao.GuiThemeJoinDaoImpl;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
@RunWith(MockitoJUnitRunner.class)
public class GuiThemeServiceImplTest {
@Mock
GuiThemeJoinDaoImpl guiThemeJoinDaoMock;
@Mock
GuiThemeJoinVO guiThemeJoinVOMock;
@Mock
EntityManager entityManagerMock;
@Mock
Object objectMock;
@Mock
ListGuiThemesCmd listGuiThemesCmdMock;
@Spy
@InjectMocks
GuiThemeServiceImpl guiThemeServiceSpy = new GuiThemeServiceImpl();
private static final String COMMON_NAME = "*acme.com,acm2.com";
private static final String DOMAIN_IDS = "1,2,3";
private static final String ACCOUNT_IDS = "4,5,6";
private static final String BLANK_STRING = "";
@Test
public void listGuiThemesTestShouldIgnoreParametersWhenItIsCalledUnauthenticated() {
Pair<List<GuiThemeJoinVO>, Integer> emptyPair = new Pair<>(new ArrayList<>(), 0);
Long accountId = Account.ACCOUNT_ID_SYSTEM;
try (MockedStatic<CallContext> callContextMocked = Mockito.mockStatic(CallContext.class)) {
CallContext callContextMock = Mockito.mock(CallContext.class);
callContextMocked.when(CallContext::current).thenReturn(callContextMock);
Mockito.doReturn(accountId).when(callContextMock).getCallingAccountId();
Mockito.doReturn(emptyPair).when(guiThemeServiceSpy).listGuiThemesWithNoAuthentication(Mockito.nullable(ListGuiThemesCmd.class));
Mockito.when(listGuiThemesCmdMock.getListOnlyDefaultTheme()).thenReturn(false);
guiThemeServiceSpy.listGuiThemes(listGuiThemesCmdMock);
Mockito.verify(guiThemeServiceSpy, Mockito.times(1)).listGuiThemesWithNoAuthentication(Mockito.nullable(ListGuiThemesCmd.class));
}
}
@Test
public void listGuiThemesTestShouldCallNormalFlowWhenAuthenticated() {
Pair<List<GuiThemeVO>, Integer> emptyPair = new Pair<>(new ArrayList<>(), 0);
Long accountId = 3L;
try (MockedStatic<CallContext> callContextMocked = Mockito.mockStatic(CallContext.class)) {
CallContext callContextMock = Mockito.mock(CallContext.class);
callContextMocked.when(CallContext::current).thenReturn(callContextMock);
Mockito.doReturn(accountId).when(callContextMock).getCallingAccountId();
Mockito.doReturn(emptyPair).when(guiThemeServiceSpy).listGuiThemesInternal(Mockito.nullable(ListGuiThemesCmd.class));
Mockito.when(listGuiThemesCmdMock.getListOnlyDefaultTheme()).thenReturn(false);
guiThemeServiceSpy.listGuiThemes(listGuiThemesCmdMock);
Mockito.verify(guiThemeServiceSpy, Mockito.times(1)).listGuiThemesInternal(Mockito.nullable(ListGuiThemesCmd.class));
}
}
@Test
public void listGuiThemesTestListOnlyDefaultThemesShouldCallFindDefaultTheme() {
Mockito.when(listGuiThemesCmdMock.getListOnlyDefaultTheme()).thenReturn(true);
guiThemeServiceSpy.listGuiThemes(listGuiThemesCmdMock);
Mockito.verify(guiThemeJoinDaoMock, Mockito.times(1)).findDefaultTheme();
}
@Test
public void shouldSetGuiThemeToPrivateTestDomainIdsAndAccountIdsAreBlankShouldReturnFalse() {
boolean result = guiThemeServiceSpy.shouldSetGuiThemeToPrivate(BLANK_STRING, BLANK_STRING);
Assert.assertFalse(result);
}
@Test
public void shouldSetGuiThemeToPrivateTestDomainIdsNotBlankAndAccountIdsIsBlankShouldReturnTrue() {
boolean result = guiThemeServiceSpy.shouldSetGuiThemeToPrivate(DOMAIN_IDS, BLANK_STRING);
Assert.assertTrue(result);
}
@Test
public void shouldSetGuiThemeToPrivateTestDomainIdsIsBlankAndAccountIdsIsNotBlankShouldReturnTrue() {
boolean result = guiThemeServiceSpy.shouldSetGuiThemeToPrivate(BLANK_STRING, ACCOUNT_IDS);
Assert.assertTrue(result);
}
@Test
public void shouldSetGuiThemeToPrivateTestDomainIdsAndAccountIdsAreNotBlankShouldReturnTrue() {
boolean result = guiThemeServiceSpy.shouldSetGuiThemeToPrivate(DOMAIN_IDS, ACCOUNT_IDS);
Assert.assertTrue(result);
}
@Test
public void validateObjectUuidsTestProvidedUuidsIsNullShouldNotThrowCloudRuntimeException() {
guiThemeServiceSpy.validateObjectUuids(null, null);
}
@Test
public void validateObjectUuidsTestProvidedUuidsIsBlankShouldNotThrowCloudRuntimeException() {
guiThemeServiceSpy.validateObjectUuids(BLANK_STRING, null);
}
@Test
public void validateObjectUuidsTestProvidedUuidsIsBlankAndCommaSeparatedShouldNotThrowCloudRuntimeException() {
String blankCommaSeparatedString = ",,,";
guiThemeServiceSpy.validateObjectUuids(blankCommaSeparatedString, null);
}
@Test
public void validateObjectUuidsTestProvidedUuidIsValidShouldNotThrowCloudRuntimeException() {
String validUuid = "4";
Mockito.when(entityManagerMock.findByUuid(Mockito.any(Class.class), Mockito.anyString())).thenReturn(objectMock);
guiThemeServiceSpy.validateObjectUuids(validUuid, Account.class);
}
@Test
public void validateObjectUuidsTestProvidedUuidsAreValidShouldNotThrowCloudRuntimeException() {
Mockito.when(entityManagerMock.findByUuid(Mockito.any(Class.class), Mockito.anyString())).thenReturn(objectMock);
guiThemeServiceSpy.validateObjectUuids(ACCOUNT_IDS, Account.class);
}
@Test(expected = CloudRuntimeException.class)
public void validateObjectUuidsTestProvidedUuidsAreNotValidShouldThrowCloudRuntimeException() {
Mockito.when(entityManagerMock.findByUuid(Mockito.any(Class.class), Mockito.anyString())).thenReturn(null);
guiThemeServiceSpy.validateObjectUuids(ACCOUNT_IDS, Account.class);
}
@Test
public void checkIfDefaultThemeIsAllowedTestThemeIsNotConsideredDefault() {
Mockito.when(guiThemeServiceSpy.isConsideredDefaultTheme(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(false);
guiThemeServiceSpy.checkIfDefaultThemeIsAllowed(COMMON_NAME, BLANK_STRING, BLANK_STRING, null);
}
@Test
public void checkIfDefaultThemeIsAllowedTestThemeIsConsideredDefaultAndThereIsNotADefaultThemeRegistered() {
Mockito.when(guiThemeServiceSpy.isConsideredDefaultTheme(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(true);
Mockito.when(guiThemeJoinDaoMock.findDefaultTheme()).thenReturn(null);
guiThemeServiceSpy.checkIfDefaultThemeIsAllowed(BLANK_STRING, BLANK_STRING, BLANK_STRING, null);
}
@Test(expected = CloudRuntimeException.class)
public void checkIfDefaultThemeIsAllowedTestThemeIsConsideredDefaultAndThereIsADefaultThemeRegisteredShouldThrowCloudRuntimeException() {
Mockito.when(guiThemeServiceSpy.isConsideredDefaultTheme(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(true);
Mockito.when(guiThemeJoinDaoMock.findDefaultTheme()).thenReturn(guiThemeJoinVOMock);
guiThemeServiceSpy.checkIfDefaultThemeIsAllowed(BLANK_STRING, BLANK_STRING, BLANK_STRING, null);
}
@Test(expected = CloudRuntimeException.class)
public void checkIfDefaultThemeIsAllowedTestThemeIsConsideredDefaultAndWillBeUpdatedAndIsDifferentIdShouldThrowCloudRuntimeException() {
Mockito.when(guiThemeServiceSpy.isConsideredDefaultTheme(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(true);
Mockito.when(guiThemeJoinDaoMock.findDefaultTheme()).thenReturn(guiThemeJoinVOMock);
Mockito.when(guiThemeJoinVOMock.getId()).thenReturn(1L);
guiThemeServiceSpy.checkIfDefaultThemeIsAllowed(BLANK_STRING, BLANK_STRING, BLANK_STRING, 2L);
}
@Test
public void checkIfDefaultThemeIsAllowedTestThemeIsConsideredDefaultAndWillBeUpdatedAndIsTheSameShouldAllowUpdate() {
Mockito.when(guiThemeServiceSpy.isConsideredDefaultTheme(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(true);
Mockito.when(guiThemeJoinDaoMock.findDefaultTheme()).thenReturn(guiThemeJoinVOMock);
Mockito.when(guiThemeJoinVOMock.getId()).thenReturn(1L);
guiThemeServiceSpy.checkIfDefaultThemeIsAllowed(BLANK_STRING, BLANK_STRING, BLANK_STRING, 1L);
}
}

View File

@ -260,7 +260,11 @@ known_categories = {
'addNodesToKubernetesCluster': 'Kubernetes Service',
'removeNodesFromKubernetesCluster': 'Kubernetes Service',
'configureStorageAccess': 'Storage Access Groups',
'listStorageAccessGroups': 'Storage Access Groups'
'listStorageAccessGroups': 'Storage Access Groups',
'listGuiThemes': 'GUI Theme',
'createGuiTheme': 'GUI Theme',
'updateGuiTheme': 'GUI Theme',
'removeGuiTheme': 'GUI Theme'
}

View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 7127.6 2890" enable-background="new 0 0 7127.6 2890" xml:space="preserve">
<path fill="#6D6E71" d="M7104.7,847.8c15.3,15.3,22.9,33.7,22.9,55.2c0,21.5-7.6,39.9-22.9,55.4c-15.3,15.4-33.8,23.1-55.6,23.1 c-21.8,0-40.2-7.6-55.4-22.9c-15.1-15.3-22.7-33.7-22.7-55.2c0-21.5,7.6-39.9,22.9-55.4c15.3-15.4,33.7-23.1,55.4-23.1 C7070.9,824.9,7089.4,832.5,7104.7,847.8z M7098.1,951.9c13.3-13.6,20-29.8,20-48.7s-6.6-35-19.8-48.5 c-13.2-13.4-29.4-20.1-48.6-20.1c-19.2,0-35.4,6.7-48.7,20.2c-13.3,13.5-19.9,29.7-19.9,48.7c0,19,6.6,35.2,19.7,48.6 c13.1,13.4,29.3,20.1,48.5,20.1S7084.7,965.4,7098.1,951.9z M7087.1,888.1c0,14-6.1,22.8-18.4,26.4l22.5,30.5h-18.2l-20.3-28.3 h-18.6v28.3h-14.7v-84.6h31.8c12.8,0,22,2.2,27.6,6.6C7084.4,871.4,7087.1,878.4,7087.1,888.1z M7068.2,900c3-2.4,4.4-6.5,4.4-12 c0-5.5-1.5-9.4-4.5-11.6c-3-2.2-8.4-3.2-16-3.2h-18v30.5h17.5C7059.7,903.6,7065.3,902.4,7068.2,900z"/>
<path fill="#6D6E71" d="M1803.6,499.8v155.4h-20V499.8h-56.8v-19.2h133.9v19.2H1803.6z"/>
<path fill="#6D6E71" d="M2082.2,655.2v-76.9h-105.2v76.9h-20V480.5h20v78.9h105.2v-78.9h20v174.7H2082.2z"/>
<path fill="#6D6E71" d="M2241.4,499.8v57.4h88.1v19.2h-88.1v59.8h101.8v19h-121.8V480.5H2340v19.2H2241.4z"/>
<path fill="#D22128" d="M1574.5,1852.4l417.3-997.6h80.1l417.3,997.6h-105.4l-129.3-311.9h-448.2l-127.9,311.9H1574.5z M2032.6,970 l-205.1,493.2h404.7L2032.6,970z"/>
<path fill="#D22128" d="M2596.9,1852.4V854.8H3010c171.4,0,295.1,158.8,295.1,313.3c0,163-115.2,316.1-286.6,316.1h-324.6v368.1 H2596.9z M2693.9,1397.1h318.9c118,0,193.9-108.2,193.9-229c0-125.1-92.7-226.2-202.3-226.2h-310.5V1397.1z"/>
<path fill="#D22128" d="M3250.5,1852.4l417.3-997.6h80.1l417.3,997.6h-105.4l-129.3-311.9h-448.2l-127.9,311.9H3250.5z M3708.6,970 l-205.1,493.2h404.7L3708.6,970z"/>
<path fill="#D22128" d="M4637.3,849.1c177,0,306.3,89.9,368.1,217.8l-78.7,47.8c-63.2-132.1-186.9-177-295.1-177 c-238.9,0-369.5,213.6-369.5,414.5c0,220.6,161.6,420.1,373.7,420.1c112.4,0,244.5-56.2,307.7-185.5l81.5,42.1 c-64.6,148.9-241.7,231.8-394.8,231.8c-274,0-466.5-261.3-466.5-514.2C4163.8,1106.3,4336.6,849.1,4637.3,849.1z"/>
<path fill="#D22128" d="M5949.1,854.8v997.6h-98.4v-466.5h-591.5v466.5h-96.9V854.8h96.9v444h591.5v-444H5949.1z"/>
<path fill="#D22128" d="M6844.6,1765.2v87.1h-670.2V854.8H6832v87.1h-560.6v359.7h489v82.9h-489v380.8H6844.6z"/>
<path fill="#6D6E71" d="M1667.6,2063.6c11.8,3.5,22.2,8.3,31,14.2l-10.3,22.6c-9-6-18.6-10.4-28.9-13.4c-10.2-2.9-20-4.4-29.2-4.4 c-13.6,0-24.5,2.4-32.6,7.3c-8.1,4.9-12.2,11.8-12.2,20.7c0,7.6,2.3,14,6.8,19c4.5,5,10.2,8.9,17,11.7c6.8,2.8,16.1,6,28,9.6 c14.4,4.6,26,8.9,34.7,12.9c8.8,4,16.3,9.9,22.5,17.8c6.2,7.8,9.3,18.2,9.3,31c0,11.7-3.2,21.8-9.5,30.6 c-6.3,8.7-15.3,15.5-26.8,20.3c-11.6,4.8-24.9,7.2-40,7.2c-15.1,0-29.7-2.9-43.9-8.7c-14.2-5.8-26.4-13.6-36.6-23.4l10.7-21.6 c9.6,9.4,20.7,16.7,33.3,21.9c12.6,5.2,24.8,7.8,36.8,7.8c15.3,0,27.3-3,36.1-8.9c8.8-5.9,13.2-13.9,13.2-23.9 c0-7.8-2.3-14.3-6.9-19.4c-4.6-5.1-10.3-9-17.1-11.9c-6.8-2.8-16.1-6-28-9.6c-14.2-4.2-25.7-8.3-34.6-12.2 c-8.9-3.9-16.4-9.7-22.5-17.5c-6.1-7.7-9.2-17.9-9.2-30.6c0-10.9,3-20.4,9-28.6c6-8.2,14.6-14.6,25.6-19.1 c11.1-4.5,23.8-6.8,38.2-6.8C1643.8,2058.3,1655.7,2060.1,1667.6,2063.6z"/>
<path fill="#6D6E71" d="M1980.1,2072.8c16.8,9.4,30.2,22.3,40,38.4c9.8,16.2,14.8,33.9,14.8,53.3c0,19.5-4.9,37.4-14.8,53.6 c-9.8,16.3-23.2,29.1-40,38.6c-16.8,9.5-35.3,14.3-55.2,14.3c-20.3,0-38.8-4.7-55.7-14.3c-16.8-9.5-30.2-22.4-40-38.6 c-9.8-16.3-14.8-34.1-14.8-53.6c0-19.5,4.9-37.3,14.8-53.5c9.8-16.2,23.2-29,40-38.3c16.8-9.4,35.4-14,55.7-14 C1944.8,2058.6,1963.2,2063.3,1980.1,2072.8z M1881.9,2092.7c-13.1,7.4-23.6,17.5-31.4,30.1c-7.8,12.6-11.8,26.5-11.8,41.7 c0,15.3,3.9,29.3,11.8,42c7.8,12.7,18.3,22.8,31.4,30.2c13.1,7.4,27.4,11.1,42.9,11.1c15.5,0,29.7-3.7,42.7-11.1 c13-7.4,23.3-17.4,31.1-30.2c7.7-12.7,11.6-26.7,11.6-42s-3.9-29.2-11.6-41.8c-7.7-12.6-18.1-22.6-31.1-30 c-13-7.4-27.2-11.2-42.6-11.2C1909.4,2081.5,1895.1,2085.2,1881.9,2092.7z"/>
<path fill="#6D6E71" d="M2186.5,2082.4v74h98.4v23.2h-98.4v90.2h-24.1v-210.6h133.8v23.2H2186.5z"/>
<path fill="#6D6E71" d="M2491.6,2082.4v187.4h-24.1v-187.4h-68.4v-23.2h161.4v23.2H2491.6z"/>
<path fill="#6D6E71" d="M2871.8,2269.8l-56.8-177.4l-57.6,177.4h-24.5l-70.5-210.6h25.9l57.9,182.7l57.1-182.4l24.1-0.3l57.7,182.7 l57.1-182.7h25l-70.6,210.6H2871.8z"/>
<path fill="#6D6E71" d="M3087.3,2216.6l-23.5,53.2h-25.6l94.4-210.6h25l94.1,210.6h-26.1l-23.5-53.2H3087.3z M3144.5,2086.6 l-46.9,106.8h94.4L3144.5,2086.6z"/>
<path fill="#6D6E71" d="M3461.1,2202.7c-6,0.4-10.7,0.6-14.1,0.6h-56v66.5H3367v-210.6h80c26.2,0,46.6,6.2,61.2,18.5 c14.5,12.3,21.8,29.8,21.8,52.3c0,17.2-4.1,31.7-12.2,43.3c-8.1,11.6-19.8,20-35,25l49.2,71.5h-27.3L3461.1,2202.7z M3491.3,2167.6 c10.3-8.4,15.5-20.8,15.5-37c0-15.9-5.2-27.9-15.5-36c-10.3-8.1-25.1-12.2-44.3-12.2h-56v97.8h56 C3466.2,2180.2,3481,2176,3491.3,2167.6z"/>
<path fill="#6D6E71" d="M3688.3,2082.4v69.2h106.2v23.2h-106.2v72.1h122.8v22.9h-146.9v-210.6h142.9v23.2H3688.3z"/>
<path fill="#6D6E71" d="M4147,2082.4v74h98.4v23.2H4147v90.2h-24.1v-210.6h133.8v23.2H4147z"/>
<path fill="#6D6E71" d="M4523.3,2072.8c16.8,9.4,30.2,22.3,40,38.4c9.8,16.2,14.8,33.9,14.8,53.3c0,19.5-4.9,37.4-14.8,53.6 c-9.8,16.3-23.2,29.1-40,38.6c-16.8,9.5-35.3,14.3-55.2,14.3c-20.3,0-38.8-4.7-55.7-14.3c-16.8-9.5-30.2-22.4-40-38.6 c-9.8-16.3-14.8-34.1-14.8-53.6c0-19.5,4.9-37.3,14.8-53.5c9.8-16.2,23.2-29,40-38.3c16.8-9.4,35.4-14,55.7-14 C4488.1,2058.6,4506.5,2063.3,4523.3,2072.8z M4425.2,2092.7c-13.1,7.4-23.6,17.5-31.4,30.1c-7.8,12.6-11.8,26.5-11.8,41.7 c0,15.3,3.9,29.3,11.8,42c7.8,12.7,18.3,22.8,31.4,30.2c13.1,7.4,27.4,11.1,42.9,11.1c15.5,0,29.7-3.7,42.7-11.1 c13-7.4,23.3-17.4,31.1-30.2c7.7-12.7,11.6-26.7,11.6-42s-3.9-29.2-11.6-41.8c-7.7-12.6-18.1-22.6-31.1-30 c-13-7.4-27.2-11.2-42.6-11.2C4452.6,2081.5,4438.3,2085.2,4425.2,2092.7z"/>
<path fill="#6D6E71" d="M4854.7,2247.7c-15.7,15.5-37.3,23.3-64.8,23.3c-27.7,0-49.4-7.8-65.1-23.3c-15.7-15.5-23.6-37-23.6-64.6 v-124h24.1v124c0,20.3,5.8,36.1,17.3,47.5c11.6,11.4,27.3,17.1,47.3,17.1c20.1,0,35.8-5.7,47.1-17c11.4-11.3,17-27.2,17-47.7v-124 h24.1v124C4878.2,2210.7,4870.4,2232.2,4854.7,2247.7z"/>
<path fill="#6D6E71" d="M5169.5,2269.8l-126.3-169.1v169.1h-24.1v-210.6h25l126.3,169.3v-169.3h23.8v210.6H5169.5z"/>
<path fill="#6D6E71" d="M5478.4,2073.1c16.4,9.3,29.4,21.9,38.9,37.9c9.6,16,14.3,33.9,14.3,53.5s-4.8,37.6-14.3,53.6 c-9.5,16.1-22.6,28.7-39.3,37.9c-16.6,9.2-35.2,13.8-55.5,13.8h-84.3v-210.6h85.2C5443.7,2059.2,5462,2063.8,5478.4,2073.1z M5362.3,2246.9h61.4c15.5,0,29.6-3.5,42.3-10.6c12.7-7.1,22.8-16.9,30.2-29.5c7.4-12.5,11.1-26.5,11.1-42 c0-15.5-3.8-29.4-11.3-41.9c-7.5-12.5-17.7-22.3-30.6-29.6c-12.8-7.2-27-10.9-42.6-10.9h-60.5V2246.9z"/>
<path fill="#6D6E71" d="M5668.6,2216.6l-23.5,53.2h-25.6l94.4-210.6h25l94.1,210.6H5807l-23.5-53.2H5668.6z M5725.8,2086.6 l-46.9,106.8h94.4L5725.8,2086.6z"/>
<path fill="#6D6E71" d="M5991,2082.4v187.4H5967v-187.4h-68.4v-23.2h161.4v23.2H5991z"/>
<path fill="#6D6E71" d="M6175.9,2269.8v-210.6h24.1v210.6H6175.9z"/>
<path fill="#6D6E71" d="M6493.7,2072.8c16.8,9.4,30.2,22.3,40,38.4c9.8,16.2,14.8,33.9,14.8,53.3c0,19.5-4.9,37.4-14.8,53.6 c-9.8,16.3-23.2,29.1-40,38.6c-16.8,9.5-35.3,14.3-55.2,14.3c-20.3,0-38.8-4.7-55.7-14.3c-16.8-9.5-30.2-22.4-40-38.6 c-9.8-16.3-14.8-34.1-14.8-53.6c0-19.5,4.9-37.3,14.8-53.5c9.8-16.2,23.2-29,40-38.3c16.8-9.4,35.4-14,55.7-14 C6458.5,2058.6,6476.9,2063.3,6493.7,2072.8z M6395.6,2092.7c-13.1,7.4-23.6,17.5-31.4,30.1c-7.8,12.6-11.8,26.5-11.8,41.7 c0,15.3,3.9,29.3,11.8,42c7.8,12.7,18.3,22.8,31.4,30.2c13.1,7.4,27.4,11.1,42.9,11.1c15.5,0,29.7-3.7,42.7-11.1 c13-7.4,23.3-17.4,31.1-30.2c7.7-12.7,11.6-26.7,11.6-42s-3.9-29.2-11.6-41.8c-7.7-12.6-18.1-22.6-31.1-30 c-13-7.4-27.2-11.2-42.6-11.2C6423,2081.5,6408.8,2085.2,6395.6,2092.7z"/>
<path fill="#6D6E71" d="M6826.5,2269.8l-126.3-169.1v169.1h-24.1v-210.6h25l126.3,169.3v-169.3h23.8v210.6H6826.5z"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-4516.6152" y1="-2338.7222" x2="-4108.4111" y2="-1861.3982" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0" style="stop-color:#F69923"/>
<stop offset="0.3123" style="stop-color:#F79A23"/>
<stop offset="0.8383" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M1230.1,13.7c-45.3,26.8-120.6,102.5-210.5,212.3l82.6,155.9c58-82.9,116.9-157.5,176.3-221.2 c4.6-5.1,7-7.5,7-7.5c-2.3,2.5-4.6,5-7,7.5c-19.2,21.2-77.5,89.2-165.5,224.4c84.7-4.2,214.9-21.6,321.1-39.7 c31.6-177-31-258-31-258S1323.4-41.4,1230.1,13.7z"/>
<path fill="none" d="M1090.2,903.1c0.6-0.1,1.2-0.2,1.8-0.3l-11.9,1.3c-0.7,0.3-1.4,0.7-2.1,1 C1082.1,904.4,1086.2,903.7,1090.2,903.1z"/>
<path fill="none" d="M1005.9,1182.3c-6.7,1.5-13.7,2.7-20.7,3.7C992.3,1185,999.2,1183.8,1005.9,1182.3z"/>
<path fill="none" d="M432.9,1808.8c0.9-2.3,1.8-4.7,2.6-7c18.2-48,36.2-94.7,54-140.1c20-51,39.8-100.4,59.3-148.3 c20.6-50.4,40.9-99.2,60.9-146.3c21-49.4,41.7-97,62-142.8c16.5-37.3,32.8-73.4,48.9-108.3c5.4-11.7,10.7-23.2,16-34.6 c10.5-22.7,21-44.8,31.3-66.5c9.5-20,19-39.6,28.3-58.8c3.1-6.4,6.2-12.8,9.3-19.1c0.5-1,1-2,1.5-3.1l-10.2,1.1l-8-15.9 c-0.8,1.6-1.6,3.1-2.4,4.6c-14.5,28.8-28.9,57.9-43.1,87.2c-8.2,16.9-16.4,34-24.6,51c-22.6,47.4-44.8,95.2-66.6,143.3 c-22.1,48.6-43.7,97.5-64.9,146.5c-20.8,48.1-41.3,96.2-61.2,144.2c-20,48-39.5,95.7-58.5,143.2c-19.9,49.5-39.2,98.7-58,147.2 c-4.2,10.9-8.5,21.9-12.7,32.8c-15,39.2-29.7,77.8-44,116l12.7,25.1l11.4-1.2c0.4-1.1,0.8-2.3,1.3-3.4 C396.7,1905.4,414.9,1856.4,432.9,1808.8z"/>
<path fill="none" d="M980,1186.8L980,1186.8c0.1,0,0.1,0,0.1-0.1C980.1,1186.8,980.1,1186.8,980,1186.8z"/>
<path fill="#BE202E" d="M952.6,1323c-10.6,1.9-21.4,3.8-32.5,5.7c-0.1,0-0.1,0.1-0.2,0.1c5.6-0.8,11.2-1.7,16.6-2.6 C942,1325.2,947.3,1324.1,952.6,1323z"/>
<path opacity="0.35" fill="#BE202E" d="M952.6,1323c-10.6,1.9-21.4,3.8-32.5,5.7c-0.1,0-0.1,0.1-0.2,0.1c5.6-0.8,11.2-1.7,16.6-2.6 C942,1325.2,947.3,1324.1,952.6,1323z"/>
<path fill="#BE202E" d="M980.3,1186.7C980.2,1186.7,980.2,1186.7,980.3,1186.7c-0.1,0.1-0.2,0.1-0.2,0.1c1.8-0.2,3.5-0.5,5.2-0.8 c7-1,13.9-2.2,20.7-3.7C997.5,1183.8,989,1185.2,980.3,1186.7L980.3,1186.7L980.3,1186.7z"/>
<path opacity="0.35" fill="#BE202E" d="M980.3,1186.7C980.2,1186.7,980.2,1186.7,980.3,1186.7c-0.1,0.1-0.2,0.1-0.2,0.1 c1.8-0.2,3.5-0.5,5.2-0.8c7-1,13.9-2.2,20.7-3.7C997.5,1183.8,989,1185.2,980.3,1186.7L980.3,1186.7L980.3,1186.7z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-7537.7339" y1="-2391.4075" x2="-4625.4141" y2="-2391.4075" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_2_)" d="M858.6,784.7c25.1-46.9,50.5-92.8,76.2-137.4c26.7-46.4,53.7-91.3,80.9-134.7 c1.6-2.6,3.2-5.2,4.8-7.7c27-42.7,54.2-83.7,81.6-122.9L1019.5,226c-6.2,7.6-12.5,15.3-18.8,23.2c-23.8,29.7-48.6,61.6-73.9,95.5 c-28.6,38.2-58,78.9-87.8,121.7c-27.6,39.5-55.5,80.9-83.5,123.7c-23.8,36.5-47.7,74-71.4,112.5c-0.9,1.4-1.8,2.9-2.6,4.3 l107.5,212.3C811.8,873.6,835.1,828.7,858.6,784.7z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-7186.1777" y1="-2099.3059" x2="-5450.7183" y2="-2099.3059" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0" style="stop-color:#282662"/>
<stop offset="9.548390e-02" style="stop-color:#662E8D"/>
<stop offset="0.7882" style="stop-color:#9F2064"/>
<stop offset="0.9487" style="stop-color:#CD2032"/>
</linearGradient>
<path fill="url(#SVGID_3_)" d="M369,1981c-14.2,39.1-28.5,78.9-42.9,119.6c-0.2,0.6-0.4,1.2-0.6,1.8c-2,5.7-4.1,11.5-6.1,17.2 c-9.7,27.4-18,52.1-37.3,108.2c31.7,14.5,57.1,52.5,81.1,95.6c-2.6-44.7-21-86.6-56.2-119.1c156.1,7,290.6-32.4,360.1-146.6 c6.2-10.2,11.9-20.9,17-32.2c-31.6,40.1-70.8,57.1-144.5,53c-0.2,0.1-0.3,0.1-0.5,0.2c0.2-0.1,0.3-0.1,0.5-0.2 c108.6-48.6,163.1-95.3,211.2-172.6c11.4-18.3,22.5-38.4,33.8-60.6c-94.9,97.5-205,125.3-320.9,104.2l-86.9,9.5 C374.4,1966.3,371.7,1973.6,369,1981z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-7374.1626" y1="-2418.5454" x2="-4461.8428" y2="-2418.5454" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_4_)" d="M409.6,1786.3c18.8-48.5,38.1-97.7,58-147.2c19-47.4,38.5-95.2,58.5-143.2 c20-48,40.4-96.1,61.2-144.2c21.2-49,42.9-97.8,64.9-146.5c21.8-48.1,44-95.9,66.6-143.3c8.1-17.1,16.3-34.1,24.6-51 c14.2-29.3,28.6-58.4,43.1-87.2c0.8-1.6,1.6-3.1,2.4-4.6L681.4,706.8c-1.8,2.9-3.5,5.8-5.3,8.6c-25.1,40.9-50,82.7-74.4,125.4 c-24.7,43.1-49,87.1-72.7,131.7c-20,37.6-39.6,75.6-58.6,113.9c-3.8,7.8-7.6,15.5-11.3,23.2c-23.4,48.2-44.6,94.8-63.7,139.5 c-21.7,50.7-40.7,99.2-57.5,145.1c-11,30.2-21,59.4-30.1,87.4c-7.5,24-14.7,47.9-21.5,71.8c-16,56.3-29.9,112.4-41.2,168.3 L353,1935.1c14.3-38.1,28.9-76.8,44-116C401.1,1808.2,405.4,1797.3,409.6,1786.3z"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="-7161.7642" y1="-2379.1431" x2="-5631.2524" y2="-2379.1431" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0" style="stop-color:#282662"/>
<stop offset="9.548390e-02" style="stop-color:#662E8D"/>
<stop offset="0.7882" style="stop-color:#9F2064"/>
<stop offset="0.9487" style="stop-color:#CD2032"/>
</linearGradient>
<path fill="url(#SVGID_5_)" d="M243.5,1729.4c-13.6,68.2-23.2,136.2-28,203.8c-0.2,2.4-0.4,4.7-0.5,7.1 c-33.7-54-124-106.8-123.8-106.2c64.6,93.7,113.7,186.7,120.9,278c-34.6,7.1-82-3.2-136.8-23.3c57.1,52.5,100,67,116.7,70.9 c-52.5,3.3-107.1,39.3-162.1,80.8c80.5-32.8,145.5-45.8,192.1-35.3C148.1,2414.2,74.1,2645,0,2890c22.7-6.7,36.2-21.9,43.9-42.6 c13.2-44.4,100.8-335.6,238-718.2c3.9-10.9,7.8-21.8,11.8-32.9c1.1-3,2.2-6.1,3.3-9.2c14.5-40.1,29.5-81.1,45.1-122.9 c3.5-9.5,7.1-19,10.7-28.6c0.1-0.2,0.1-0.4,0.2-0.6l-107.9-213.2C244.6,1724.4,244,1726.9,243.5,1729.4z"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-7374.1626" y1="-2117.1309" x2="-4461.8428" y2="-2117.1309" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_6_)" d="M805.6,937c-3.1,6.3-6.2,12.7-9.3,19.1c-9.3,19.2-18.8,38.8-28.3,58.8 c-10.3,21.7-20.7,43.9-31.3,66.5c-5.3,11.4-10.6,22.9-16,34.6c-16.1,35-32.4,71.1-48.9,108.3c-20.3,45.8-41,93.4-62,142.8 c-20,47.1-40.3,95.9-60.9,146.3c-19.5,47.9-39.3,97.3-59.3,148.3c-17.8,45.4-35.9,92.1-54,140.1c-0.9,2.3-1.8,4.7-2.6,7 c-18,47.6-36.2,96.6-54.6,146.8c-0.4,1.1-0.8,2.3-1.3,3.4l86.9-9.5c-1.7-0.3-3.5-0.5-5.2-0.9c103.9-13,242.1-90.6,331.4-186.5 c41.1-44.2,78.5-96.3,113-157.3c25.7-45.4,49.8-95.8,72.8-151.5c20.1-48.7,39.4-101.4,58-158.6c-23.9,12.6-51.2,21.8-81.4,28.2 c-5.3,1.1-10.7,2.2-16.1,3.1c-5.5,1-11,1.8-16.6,2.6l0,0l0,0c0.1,0,0.1-0.1,0.2-0.1c96.9-37.3,158-109.2,202.4-197.4 c-25.5,17.4-66.9,40.1-116.6,51.1c-6.7,1.5-13.7,2.7-20.7,3.7c-1.7,0.3-3.5,0.6-5.2,0.8l0,0l0,0c0.1,0,0.1,0,0.1-0.1 c0,0,0.1,0,0.1,0l0,0c33.6-14.1,62-29.8,86.6-48.4c5.3-4,10.4-8.1,15.3-12.3c7.5-6.5,14.7-13.3,21.5-20.5c4.4-4.6,8.6-9.3,12.7-14.2 c9.6-11.5,18.7-23.9,27.1-37.3c2.6-4.1,5.1-8.3,7.6-12.6c3.2-6.2,6.3-12.3,9.3-18.3c13.5-27.2,24.4-51.5,33-72.8 c4.3-10.6,8.1-20.5,11.3-29.7c1.3-3.7,2.5-7.2,3.7-10.6c3.4-10.2,6.2-19.3,8.4-27.3c3.3-12,5.3-21.5,6.4-28.4l0,0l0,0 c-3.3,2.6-7.1,5.2-11.3,7.7c-29.3,17.5-79.5,33.4-119.9,40.8l79.8-8.8l-79.8,8.8c-0.6,0.1-1.2,0.2-1.8,0.3c-4,0.7-8.1,1.3-12.2,2 c0.7-0.3,1.4-0.7,2.1-1l-273,29.9C806.6,935,806.1,936,805.6,937z"/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-7554.8232" y1="-2132.0981" x2="-4642.5034" y2="-2132.0981" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_7_)" d="M1112.9,385.1c-24.3,37.3-50.8,79.6-79.4,127.5c-1.5,2.5-3,5.1-4.5,7.6 c-24.6,41.5-50.8,87.1-78.3,137c-23.8,43.1-48.5,89.3-74.3,139c-22.4,43.3-45.6,89.2-69.4,137.8l273-29.9 c79.5-36.6,115.1-69.7,149.6-117.6c9.2-13.2,18.4-27,27.5-41.3c28-43.8,55.6-92,80.1-139.9c23.7-46.3,44.7-92.2,60.7-133.5 c10.2-26.3,18.4-50.8,24.1-72.3c5-19,8.9-36.9,11.9-54.1C1327.9,363.5,1197.6,380.9,1112.9,385.1z"/>
<path fill="#BE202E" d="M936.5,1326.1c-5.5,1-11,1.8-16.6,2.6l0,0C925.5,1328,931,1327.1,936.5,1326.1z"/>
<path opacity="0.35" fill="#BE202E" d="M936.5,1326.1c-5.5,1-11,1.8-16.6,2.6l0,0C925.5,1328,931,1327.1,936.5,1326.1z"/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="-7374.1626" y1="-2027.484" x2="-4461.8433" y2="-2027.484" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_8_)" d="M936.5,1326.1c-5.5,1-11,1.8-16.6,2.6l0,0C925.5,1328,931,1327.1,936.5,1326.1z"/>
<path fill="#BE202E" d="M980,1186.8c1.8-0.2,3.5-0.5,5.2-0.8C983.5,1186.3,981.8,1186.6,980,1186.8L980,1186.8z"/>
<path opacity="0.35" fill="#BE202E" d="M980,1186.8c1.8-0.2,3.5-0.5,5.2-0.8C983.5,1186.3,981.8,1186.6,980,1186.8L980,1186.8z"/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="-7374.1626" y1="-2037.7417" x2="-4461.8433" y2="-2037.7417" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_9_)" d="M980,1186.8c1.8-0.2,3.5-0.5,5.2-0.8C983.5,1186.3,981.8,1186.6,980,1186.8L980,1186.8z"/>
<path fill="#BE202E" d="M980.2,1186.7C980.2,1186.7,980.2,1186.7,980.2,1186.7L980.2,1186.7L980.2,1186.7L980.2,1186.7 C980.2,1186.7,980.2,1186.7,980.2,1186.7z"/>
<path opacity="0.35" fill="#BE202E" d="M980.2,1186.7C980.2,1186.7,980.2,1186.7,980.2,1186.7L980.2,1186.7L980.2,1186.7 L980.2,1186.7C980.2,1186.7,980.2,1186.7,980.2,1186.7z"/>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="-5738.0635" y1="-2039.799" x2="-5094.3457" y2="-2039.799" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_10_)" d="M980.2,1186.7C980.2,1186.7,980.2,1186.7,980.2,1186.7L980.2,1186.7L980.2,1186.7L980.2,1186.7 C980.2,1186.7,980.2,1186.7,980.2,1186.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Feather" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2392.5 4226.6" enable-background="new 0 0 2392.5 4226.6" xml:space="preserve">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-5167.0962" y1="697.5549" x2="-4570.1162" y2="1395.619" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0" style="stop-color:#F69923"/>
<stop offset="0.3123" style="stop-color:#F79A23"/>
<stop offset="0.8383" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M1798.9,20.1C1732.6,59.2,1622.5,170,1491,330.5l120.8,228c84.8-121.3,170.9-230.4,257.8-323.6 c6.7-7.4,10.2-10.9,10.2-10.9c-3.4,3.6-6.8,7.3-10.2,10.9c-28.1,31-113.4,130.5-242.1,328.1c123.9-6.2,314.3-31.5,469.6-58.1 c46.2-258.8-45.3-377.3-45.3-377.3S1935.5-60.6,1798.9,20.1z"/>
<path fill="none" d="M1594.4,1320.7c0.9-0.2,1.8-0.3,2.7-0.5l-17.4,1.9c-1.1,0.5-2,1-3.1,1.4 C1582.6,1322.6,1588.5,1321.6,1594.4,1320.7z"/>
<path fill="none" d="M1471.1,1729.1c-9.9,2.2-20,3.9-30.2,5.4C1451.1,1733,1461.2,1731.2,1471.1,1729.1z"/>
<path fill="none" d="M633.1,2645.2c1.3-3.4,2.6-6.8,3.8-10.2c26.6-70.2,52.9-138.4,79-204.9c29.3-74.6,58.2-146.8,86.8-216.8 c30.1-73.8,59.8-145.1,89.1-214c30.7-72.3,61-141.9,90.7-208.9c24.2-54.5,48-107.3,71.5-158.4c7.8-17,15.6-33.9,23.4-50.6 c15.4-33.1,30.7-65.6,45.7-97.3c13.9-29.3,27.7-57.9,41.4-86c4.5-9.4,9.1-18.6,13.6-27.9c0.7-1.5,1.5-3,2.2-4.5l-14.8,1.6 l-11.8-23.2c-1.1,2.3-2.3,4.5-3.5,6.8c-21.2,42.1-42.2,84.6-63,127.5c-12,24.8-24,49.7-35.9,74.7c-33,69.3-65.5,139.2-97.4,209.6 c-32.3,71.1-63.9,142.6-94.9,214.2c-30.5,70.3-60.3,140.7-89.6,210.9c-29.2,70.1-57.7,140-85.6,209.4 c-29.1,72.5-57.4,144.3-84.8,215.3c-6.2,16-12.4,32-18.5,48c-22,57.3-43.4,113.8-64.3,169.6l18.6,36.7l16.6-1.8 c0.6-1.7,1.2-3.4,1.8-5C580.1,2786.5,606.7,2714.9,633.1,2645.2z"/>
<path fill="none" d="M1433.2,1735.7L1433.2,1735.7c0.1,0,0.1-0.1,0.2-0.1C1433.4,1735.6,1433.3,1735.6,1433.2,1735.7z"/>
<path fill="#BE202E" d="M1393.2,1934.8c-15.4,2.8-31.3,5.5-47.6,8.3c-0.1,0-0.2,0.1-0.3,0.1c8.2-1.2,16.3-2.4,24.3-3.8 C1377.6,1938,1385.4,1936.5,1393.2,1934.8z"/>
<path opacity="0.35" fill="#BE202E" d="M1393.2,1934.8c-15.4,2.8-31.3,5.5-47.6,8.3c-0.1,0-0.2,0.1-0.3,0.1 c8.2-1.2,16.3-2.4,24.3-3.8C1377.6,1938,1385.4,1936.5,1393.2,1934.8z"/>
<path fill="#BE202E" d="M1433.6,1735.5c0,0-0.1,0-0.1,0.1c-0.1,0-0.1,0.1-0.2,0.1c2.6-0.3,5.1-0.8,7.6-1.1 c10.3-1.5,20.4-3.3,30.2-5.4C1458.8,1731.2,1446.3,1733.4,1433.6,1735.5L1433.6,1735.5L1433.6,1735.5z"/>
<path opacity="0.35" fill="#BE202E" d="M1433.6,1735.5c0,0-0.1,0-0.1,0.1c-0.1,0-0.1,0.1-0.2,0.1c2.6-0.3,5.1-0.8,7.6-1.1 c10.3-1.5,20.4-3.3,30.2-5.4C1458.8,1731.2,1446.3,1733.4,1433.6,1735.5L1433.6,1735.5L1433.6,1735.5z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-9585.3418" y1="620.5048" x2="-5326.209" y2="620.5048" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_2_)" d="M1255.7,1147.6c36.7-68.6,73.9-135.7,111.5-201c39-67.8,78.5-133.6,118.4-197c2.3-3.7,4.7-7.5,7-11.3 c39.4-62.4,79.2-122.4,119.3-179.8l-120.8-228c-9.1,11.1-18.2,22.4-27.5,33.9c-34.8,43.4-71,90.1-108.1,139.6 c-41.8,55.8-84.8,115.4-128.5,177.9c-40.3,57.8-81.2,118.3-122.1,180.9c-34.8,53.3-69.8,108.2-104.5,164.5c-1.3,2.1-2.6,4.2-3.9,6.3 l157.2,310.5C1187.3,1277.6,1221.3,1212,1255.7,1147.6z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-9071.207" y1="1047.6898" x2="-6533.1782" y2="1047.6898" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0" style="stop-color:#282662"/>
<stop offset="9.548390e-02" style="stop-color:#662E8D"/>
<stop offset="0.7882" style="stop-color:#9F2064"/>
<stop offset="0.9487" style="stop-color:#CD2032"/>
</linearGradient>
<path fill="url(#SVGID_3_)" d="M539.7,2897.1c-20.8,57.2-41.7,115.4-62.7,174.9c-0.3,0.9-0.6,1.7-0.9,2.6c-3,8.4-5.9,16.8-8.9,25.2 c-14.1,40.1-26.4,76.2-54.5,158.3c46.3,21.1,83.5,76.7,118.7,139.8c-3.7-65.3-30.8-126.7-82.1-174.2 c228.3,10.3,425-47.4,526.7-214.3c9.1-14.9,17.4-30.5,24.9-47.2c-46.2,58.6-103.5,83.5-211.4,77.4c-0.2,0.1-0.5,0.2-0.7,0.3 c0.2-0.1,0.5-0.2,0.7-0.3c158.8-71.1,238.5-139.3,308.9-252.4c16.7-26.8,32.9-56.1,49.5-88.6C1009,2841.2,848.1,2881.8,678.6,2851 l-127.1,13.9C547.5,2875.6,543.6,2886.3,539.7,2897.1z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-9346.126" y1="580.817" x2="-5086.9941" y2="580.817" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_4_)" d="M599,2612.4c27.5-71,55.8-142.8,84.8-215.3c27.8-69.4,56.4-139.2,85.6-209.4 c29.2-70.2,59.1-140.5,89.6-210.9c31-71.6,62.7-143.1,94.9-214.2c31.9-70.3,64.4-140.3,97.4-209.6c11.9-25,23.9-49.9,35.9-74.7 c20.8-42.9,41.8-85.4,63-127.5c1.1-2.3,2.3-4.5,3.5-6.8l-157.2-310.5c-2.6,4.2-5.1,8.4-7.7,12.6c-36.6,59.8-73.1,121-108.9,183.5 c-36.2,63.1-71.7,127.4-106.4,192.6c-29.3,55-57.9,110.5-85.7,166.5c-5.6,11.4-11.1,22.6-16.6,33.9 c-34.3,70.5-65.2,138.6-93.2,204.1c-31.7,74.2-59.6,145.1-84,212.3c-16.1,44.2-30.7,86.9-44.1,127.9c-11,35-21.5,70.1-31.4,105 c-23.5,82.3-43.7,164.4-60.3,246.2L516.2,2830c20.9-55.8,42.3-112.3,64.3-169.6C586.6,2644.5,592.8,2628.4,599,2612.4z"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="-9035.5029" y1="638.4408" x2="-6797.2012" y2="638.4408" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0" style="stop-color:#282662"/>
<stop offset="9.548390e-02" style="stop-color:#662E8D"/>
<stop offset="0.7882" style="stop-color:#9F2064"/>
<stop offset="0.9487" style="stop-color:#CD2032"/>
</linearGradient>
<path fill="url(#SVGID_5_)" d="M356.1,2529.2c-19.8,99.8-33.9,199.2-41,298c-0.2,3.5-0.6,6.9-0.8,10.4 c-49.3-79-181.3-156.1-181-155.4c94.5,137,166.2,273,176.9,406.5c-50.6,10.4-119.9-4.6-200-34.1c83.5,76.7,146.2,97.9,170.6,103.6 c-76.7,4.8-156.6,57.5-237.1,118.2c117.7-48,212.8-67,280.9-51.6C216.6,3530.6,108.3,3868.2,0,4226.6c33.2-9.8,53-32.1,64.1-62.3 c19.3-64.9,147.4-490.7,348.1-1050.4c5.7-15.9,11.5-31.9,17.3-48c1.6-4.5,3.3-9,4.9-13.4c21.2-58.7,43.2-118.6,65.9-179.7 c5.2-13.9,10.4-27.8,15.6-41.8c0.1-0.3,0.2-0.6,0.3-0.8l-157.8-311.8C357.7,2521.9,356.8,2525.5,356.1,2529.2z"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-9346.126" y1="1021.6218" x2="-5086.9941" y2="1021.6218" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_6_)" d="M1178.1,1370.3c-4.5,9.2-9,18.5-13.6,27.9c-13.6,28.1-27.4,56.7-41.4,86 c-15.1,31.7-30.3,64.1-45.7,97.3c-7.8,16.7-15.5,33.5-23.4,50.6c-23.5,51.1-47.3,103.9-71.5,158.4c-29.7,67-60,136.6-90.7,208.9 c-29.3,68.9-59,140.2-89.1,214c-28.6,70-57.5,142.3-86.8,216.8c-26.1,66.5-52.4,134.7-79,204.9c-1.3,3.4-2.6,6.8-3.8,10.2 c-26.4,69.7-53,141.3-79.8,214.7c-0.6,1.7-1.2,3.4-1.8,5l127.1-13.9c-2.5-0.5-5.1-0.8-7.6-1.3c152-18.9,354-132.5,484.6-272.7 c60.2-64.6,114.8-140.8,165.3-230c37.6-66.4,72.9-140,106.5-221.5c29.4-71.2,57.6-148.3,84.8-231.9c-34.9,18.4-74.9,31.9-119,41.3 c-7.7,1.6-15.6,3.2-23.6,4.6c-8,1.4-16.1,2.7-24.3,3.8l0,0l0,0c0.1,0,0.2-0.1,0.3-0.1c141.7-54.5,231.1-159.8,296.1-288.7 c-37.3,25.4-97.9,58.7-170.5,74.7c-9.9,2.2-20,3.9-30.2,5.4c-2.6,0.4-5.1,0.8-7.6,1.1l0,0l0,0c0.1,0,0.1-0.1,0.2-0.1 c0,0,0.1,0,0.1-0.1l0,0c49.2-20.6,90.7-43.6,126.7-70.8c7.7-5.8,15.2-11.8,22.4-18.1c11-9.5,21.4-19.5,31.4-30 c6.4-6.7,12.6-13.6,18.6-20.8c14.1-16.8,27.3-34.9,39.7-54.6c3.8-6,7.5-12.1,11.2-18.4c4.7-9.1,9.2-18,13.6-26.8 c19.8-39.8,35.6-75.3,48.2-106.5c6.3-15.6,11.8-30,16.5-43.4c1.9-5.3,3.7-10.5,5.4-15.5c5-15,9.1-28.3,12.3-40 c4.8-17.5,7.7-31.4,9.3-41.5l0,0l0,0c-4.8,3.8-10.3,7.6-16.5,11.3c-42.8,25.6-116.2,48.8-175.4,59.7l116.7-12.8l-116.7,12.8 c-0.9,0.2-1.8,0.3-2.7,0.5c-5.9,1-11.9,1.9-17.9,2.9c1.1-0.5,2-1,3.1-1.4l-399.3,43.8C1179.6,1367.4,1178.9,1368.8,1178.1,1370.3z"/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-9610.334" y1="999.733" x2="-5351.2017" y2="999.733" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_7_)" d="M1627.6,563.1c-35.5,54.5-74.3,116.4-116,186.5c-2.2,3.6-4.4,7.4-6.6,11.1 c-36,60.7-74.3,127.3-114.5,200.3c-34.8,63-71,130.6-108.6,203.3c-32.8,63.3-66.7,130.5-101.5,201.6l399.3-43.8 c116.3-53.5,168.3-101.9,218.8-171.9c13.4-19.3,26.9-39.5,40.3-60.4c41-64,81.2-134.5,117.2-204.6c34.7-67.7,65.3-134.8,88.8-195.3 c14.9-38.5,26.9-74.3,35.2-105.7c7.3-27.7,13-54,17.4-79.1C1941.9,531.6,1751.5,557,1627.6,563.1z"/>
<path fill="#BE202E" d="M1369.6,1939.4c-8,1.4-16.1,2.7-24.3,3.8l0,0C1353.5,1942.1,1361.6,1940.8,1369.6,1939.4z"/>
<path opacity="0.35" fill="#BE202E" d="M1369.6,1939.4c-8,1.4-16.1,2.7-24.3,3.8l0,0C1353.5,1942.1,1361.6,1940.8,1369.6,1939.4z"/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="-9346.126" y1="1152.7261" x2="-5086.9941" y2="1152.7261" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_8_)" d="M1369.6,1939.4c-8,1.4-16.1,2.7-24.3,3.8l0,0C1353.5,1942.1,1361.6,1940.8,1369.6,1939.4z"/>
<path fill="#BE202E" d="M1433.2,1735.7c2.6-0.3,5.1-0.8,7.6-1.1C1438.3,1734.9,1435.8,1735.3,1433.2,1735.7L1433.2,1735.7z"/>
<path opacity="0.35" fill="#BE202E" d="M1433.2,1735.7c2.6-0.3,5.1-0.8,7.6-1.1C1438.3,1734.9,1435.8,1735.3,1433.2,1735.7 L1433.2,1735.7z"/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="-9346.126" y1="1137.7247" x2="-5086.9941" y2="1137.7247" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_9_)" d="M1433.2,1735.7c2.6-0.3,5.1-0.8,7.6-1.1C1438.3,1734.9,1435.8,1735.3,1433.2,1735.7L1433.2,1735.7z"/>
<path fill="#BE202E" d="M1433.5,1735.6c0,0,0.1,0,0.1-0.1l0,0l0,0l0,0C1433.6,1735.5,1433.5,1735.5,1433.5,1735.6z"/>
<path opacity="0.35" fill="#BE202E" d="M1433.5,1735.6c0,0,0.1,0,0.1-0.1l0,0l0,0l0,0C1433.6,1735.5,1433.5,1735.5,1433.5,1735.6z"/>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="-6953.4072" y1="1134.7161" x2="-6011.9995" y2="1134.7161" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 3144.8108 -4619.2983)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_10_)" d="M1433.5,1735.6c0,0,0.1,0,0.1-0.1l0,0l0,0l0,0C1433.6,1735.5,1433.5,1735.5,1433.5,1735.6z"/>
<path fill="#6D6E71" d="M2218.7,387v11.7h27.4v77.4h12.7v-77.4h27.6V387H2218.7z M2377,387l-30.6,62.2l-30.8-62.2h-15.3v89.1h11.7 v-70.7l30.3,61.3h8.1l30.3-61.3v70.7h11.7V387H2377z"/>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -17,6 +17,7 @@
"logo": "assets/logo.svg",
"minilogo": "assets/mini-logo.svg",
"banner": "assets/banner.svg",
"favicon": "cloud.ico",
"error": {
"403": "assets/403.png",
"404": "assets/404.png",

File diff suppressed because it is too large Load Diff

1592
ui/public/css/dark-theme.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -54,12 +54,4 @@
<div class="loader"></div>
</div>
</body>
<script type="text/javascript">
fetch('./config.json?ts=' + Date.now())
.then(response => response.json())
.then(data => {
document.getElementById("favicon").setAttribute("href", data.loginFavicon);
document.getElementById("title").innerHTML = data.loginTitle;
}).catch((err) => {});
</script>
</html>

View File

@ -88,10 +88,13 @@ export function login (arg) {
})
}
export function logout () {
message.destroy()
notification.destroy()
return postAPI('logout')
export async function logout () {
const result = await postAPI('logout').finally(() => {
sourceToken.cancel()
message.destroy()
notification.destroy()
})
return result
}
export function oauthlogin (arg) {

View File

@ -80,6 +80,8 @@ import { mapActions, mapGetters } from 'vuex'
import ResourceIcon from '@/components/view/ResourceIcon'
import eventBus from '@/config/eventBus'
import { SERVER_MANAGER } from '@/store/mutation-types'
import { sourceToken } from '@/utils/request'
import { applyCustomGuiTheme } from '@/utils/guiTheme'
export default {
name: 'UserMenu',
@ -178,7 +180,9 @@ export default {
}
},
handleLogout () {
return this.Logout({}).then(() => {
this.Logout({}).finally(async () => {
sourceToken.init()
await applyCustomGuiTheme(null, null)
this.$router.push('/user/login')
}).catch(err => {
this.$message.error({

View File

@ -43,6 +43,9 @@ import {
} from './utils/plugins'
import { VueAxios } from './utils/request'
import directives from './utils/directives'
import Cookies from 'js-cookie'
import { api } from '@/api'
import { applyCustomGuiTheme } from './utils/guiTheme'
vueApp.use(VueAxios, router)
vueApp.use(pollJobPlugin)
@ -89,7 +92,7 @@ fetch('config.json?ts=' + Date.now())
}
return response.json()
})
.then(config => {
.then(async config => {
vueProps.$config = config
let baseUrl = config.apiBase
if (config.multipleServer) {
@ -98,6 +101,19 @@ fetch('config.json?ts=' + Date.now())
vueProps.axios.defaults.baseURL = baseUrl
const userid = Cookies.get('userid')
let accountid = null
let domainid = null
if (userid !== undefined && Cookies.get('sessionkey')) {
await api('listUsers', { userid: userid }).then(response => {
accountid = response.listusersresponse.user[0].accountid
domainid = response.listusersresponse.user[0].domainid
})
}
await applyCustomGuiTheme(accountid, domainid)
loadLanguageAsync().then(() => {
vueApp.use(store)
.use(router)

View File

@ -47,6 +47,10 @@ import {
LATEST_CS_VERSION
} from '@/store/mutation-types'
import {
applyCustomGuiTheme
} from '@/utils/guiTheme'
const user = {
state: {
token: '',
@ -243,7 +247,6 @@ const user = {
const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
commit('SET_LATEST_VERSION', latestVersion)
notification.destroy()
resolve()
}).catch(error => {
reject(error)
@ -406,6 +409,7 @@ const user = {
getAPI('listUsers', { id: Cookies.get('userid'), showicon: true }).then(response => {
const result = response.listusersresponse.user[0]
applyCustomGuiTheme(result.accountid, result.domainid)
commit('SET_INFO', result)
commit('SET_NAME', result.firstname + ' ' + result.lastname)
commit('SET_AVATAR', result.icon?.base64image || '')

100
ui/src/utils/guiTheme.js Normal file
View File

@ -0,0 +1,100 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { vueProps } from '@/vue-app'
import { api } from '@/api'
export async function applyCustomGuiTheme (accountid, domainid) {
await fetch('config.json').then(response => response.json()).then(config => {
vueProps.$config = config
})
let guiTheme
if (accountid != null) {
guiTheme = await fetchGuiTheme({ accountid: accountid })
}
if (guiTheme === undefined && domainid != null) {
guiTheme = await fetchGuiTheme({ domainid: domainid })
}
if (guiTheme === undefined) {
guiTheme = await fetchGuiTheme({ commonname: window.location.hostname })
}
if (guiTheme === undefined) {
guiTheme = await fetchGuiTheme({ listonlydefaulttheme: true })
}
await applyDynamicCustomization(guiTheme)
}
async function fetchGuiTheme (params) {
return await api('listGuiThemes', params).then(response => {
if (response.listguithemesresponse.guiThemes) {
return response.listguithemesresponse.guiThemes[0]
}
})
}
async function applyDynamicCustomization (response) {
let jsonConfig
if (response?.jsonconfiguration) {
jsonConfig = JSON.parse(response?.jsonconfiguration)
}
// Sets custom GUI fields only if is not nullish.
vueProps.$config.appTitle = jsonConfig?.appTitle ?? vueProps.$config.appTitle
vueProps.$config.footer = jsonConfig?.footer ?? vueProps.$config.footer
vueProps.$config.loginFooter = jsonConfig?.loginFooter ?? vueProps.$config.loginFooter
vueProps.$config.logo = jsonConfig?.logo ?? vueProps.$config.logo
vueProps.$config.minilogo = jsonConfig?.minilogo ?? vueProps.$config.minilogo
vueProps.$config.banner = jsonConfig?.banner ?? vueProps.$config.banner
if (jsonConfig?.error) {
vueProps.$config.error[403] = jsonConfig?.error[403] ?? vueProps.$config.error[403]
vueProps.$config.error[404] = jsonConfig?.error[404] ?? vueProps.$config.error[404]
vueProps.$config.error[500] = jsonConfig?.error[500] ?? vueProps.$config.error[500]
}
if (jsonConfig?.plugins) {
jsonConfig.plugins.forEach(plugin => {
vueProps.$config.plugins.push(plugin)
})
}
vueProps.$config.favicon = jsonConfig?.favicon ?? vueProps.$config.favicon
vueProps.$config.css = response?.css ?? null
await applyStaticCustomization(vueProps.$config.favicon, vueProps.$config.css)
}
async function applyStaticCustomization (favicon, css) {
document.getElementById('favicon').href = favicon
let style = document.getElementById('guiThemeCSS')
if (style != null) {
style.innerHTML = css
} else {
style = document.createElement('style')
style.setAttribute('id', 'guiThemeCSS')
style.innerHTML = css
document.body.appendChild(style)
}
}