diff --git a/console2/src/api/service/console/user/index.ts b/console2/src/api/service/console/user/index.ts index da92dbb1c4..84e572a4b0 100644 --- a/console2/src/api/service/console/user/index.ts +++ b/console2/src/api/service/console/user/index.ts @@ -34,6 +34,7 @@ export interface ProcessCardEntry { name: string; description?: string; icon?: string; + isCustomForm: boolean; } export const getActivity = ( diff --git a/console2/src/components/organisms/UserProcessActivity/index.tsx b/console2/src/components/organisms/UserProcessActivity/index.tsx index 7dcda7ab8c..8e7b25fcef 100644 --- a/console2/src/components/organisms/UserProcessActivity/index.tsx +++ b/console2/src/components/organisms/UserProcessActivity/index.tsx @@ -24,7 +24,7 @@ import { getActivity as apiGetActivity, listProcessCards as apiListProcessCards, ProcessCardEntry } from '../../../api/service/console/user'; -import { Button, Card, CardGroup, Header, Icon, Image } from 'semantic-ui-react'; +import { Button, Card, CardGroup, Embed, Header, Icon, Image, Modal } from 'semantic-ui-react'; import { ProcessList } from '../../molecules/index'; import { ProcessEntry } from '../../../api/process'; import { @@ -64,7 +64,20 @@ const renderCard = (card: ProcessCardEntry) => {
- + {card.isCustomForm && + Start process}> + + + + + } + + {!card.isCustomForm && + + }
diff --git a/server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.1.0.xml b/server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.1.0.xml index 1e09932bd6..73d3e79736 100644 --- a/server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.1.0.xml +++ b/server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.1.0.xml @@ -93,4 +93,14 @@ onDelete="CASCADE"/> + + + + + + + + + + diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardEntry.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardEntry.java index e0a2d2faa7..77d97adc42 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardEntry.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardEntry.java @@ -64,6 +64,8 @@ public interface ProcessCardEntry extends Serializable { @Nullable String icon(); + boolean isCustomForm(); + static ImmutableProcessCardEntry.Builder builder() { return ImmutableProcessCardEntry.builder(); } diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResourceV2.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResourceV2.java index ada32d8234..c57953cb6e 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResourceV2.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResourceV2.java @@ -20,11 +20,14 @@ * ===== */ +import com.walmartlabs.concord.common.IOUtils; import com.walmartlabs.concord.db.AbstractDao; import com.walmartlabs.concord.db.MainDB; +import com.walmartlabs.concord.server.ConcordObjectMapper; import com.walmartlabs.concord.server.process.ProcessEntry; import com.walmartlabs.concord.server.process.queue.ProcessFilter; import com.walmartlabs.concord.server.process.queue.ProcessQueueDao; +import com.walmartlabs.concord.server.sdk.ConcordApplicationException; import com.walmartlabs.concord.server.sdk.metrics.WithTimer; import com.walmartlabs.concord.server.security.UserPrincipal; import org.jooq.*; @@ -33,29 +36,43 @@ import javax.inject.Inject; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; -import java.util.Base64; -import java.util.List; -import java.util.UUID; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.*; +import java.util.function.Function; import static com.walmartlabs.concord.server.jooq.Tables.*; import static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS; import static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS; +import static org.jooq.impl.DSL.*; -@Path("/api/v2/service/console/user") +@javax.ws.rs.Path("/api/v2/service/console/user") public class UserActivityResourceV2 implements Resource { + private static final String DATA_FILE_TEMPLATE = "data = %s;"; + private final ProcessQueueDao processDao; private final UserActivityDao dao; + private final ConcordObjectMapper objectMapper; @Inject public UserActivityResourceV2(ProcessQueueDao processDao, - UserActivityDao dao) { + UserActivityDao dao, + ConcordObjectMapper objectMapper) { this.processDao = processDao; this.dao = dao; + this.objectMapper = objectMapper; } @GET - @Path("/activity") + @javax.ws.rs.Path("/activity") @Produces(MediaType.APPLICATION_JSON) @WithTimer public UserActivityResponse activity(@QueryParam("maxOwnProcesses") @DefaultValue("5") int maxOwnProcesses) { @@ -73,7 +90,7 @@ public UserActivityResponse activity(@QueryParam("maxOwnProcesses") @DefaultValu } @GET - @Path("/process-card") + @javax.ws.rs.Path("/process-card") @Produces(MediaType.APPLICATION_JSON) @WithTimer public List processCardsList() { @@ -83,17 +100,131 @@ public List processCardsList() { return dao.listCards(user.getId()); } + @GET + @javax.ws.rs.Path("/process-card/{cardId}/form") + @Produces(MediaType.TEXT_HTML) + @WithTimer + public Response processForm(@PathParam("cardId") UUID cardId) { + + Optional o = dao.getForm(cardId, src -> { + try { + Path tmp = IOUtils.createTempFile("process-form", ".html"); + Files.copy(src, tmp, StandardCopyOption.REPLACE_EXISTING); + return Optional.of(tmp); + } catch (IOException e) { + throw new ConcordApplicationException("Error while downloading custom process start form: " + cardId, e); + } + }); + + if (!o.isPresent()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + return toBinaryResponse(o.get()); + } + + @GET + @javax.ws.rs.Path("/process-card/{cardId}/data.js") + @Produces("text/javascript") + @WithTimer + public Response processFormData(@PathParam("cardId") UUID cardId) { + ProcessCardEntry card = dao.get(cardId); + if (card ==null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + Map customData = dao.getFormData(cardId); + + Map resultData = new HashMap<>(customData != null ? customData : Collections.emptyMap()); + resultData.put("org", card.orgName()); + resultData.put("project", card.projectName()); + resultData.put("repo", card.repoName()); + resultData.put("entryPoint", card.entryPoint()); + + return Response.ok(formatData(resultData)) + .build(); + } + + private String formatData(Map data) { + return String.format(DATA_FILE_TEMPLATE, objectMapper.toString(data)); + } + + private static Response toBinaryResponse(Path file) { + return Response.ok((StreamingOutput) out -> { + try (InputStream in = Files.newInputStream(file)) { + IOUtils.copy(in, out); + } finally { + Files.delete(file); + } + }).build(); + } + public static class UserActivityDao extends AbstractDao { + private final ConcordObjectMapper objectMapper; + @Inject - protected UserActivityDao(@MainDB Configuration cfg) { + protected UserActivityDao(@MainDB Configuration cfg, ConcordObjectMapper objectMapper) { super(cfg); + this.objectMapper = objectMapper; + } + + public ProcessCardEntry get(UUID cardId) { + return txResult(tx -> get(tx, cardId)); + } + + public ProcessCardEntry get(DSLContext tx, UUID cardId) { + return buildSelect(tx) + .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId)) + .fetchOne(this::toEntry); } public List listCards(UUID userId) { return txResult(tx -> listCards(tx, userId)); } + public Map getFormData(UUID cardId) { + return txResult(tx -> getFormData(tx, cardId)); + } + + public Map getFormData(DSLContext tx, UUID cardId) { + return tx.select(UI_PROCESS_CARDS.DATA) + .from(UI_PROCESS_CARDS) + .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId)) + .fetchOne(r -> objectMapper.fromJSONB(r.get(UI_PROCESS_CARDS.DATA))); + } + + public Optional getForm(UUID cardId, Function> converter) { + return txResult(tx -> getForm(tx, cardId, converter)); + } + + public Optional getForm(DSLContext tx, UUID cardId, Function> converter) { + String sql = tx.select(UI_PROCESS_CARDS.FORM) + .from(UI_PROCESS_CARDS) + .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq((UUID) null) + .and(UI_PROCESS_CARDS.FORM.isNotNull())) + .getSQL(); + + return getInputStream(tx, sql, cardId, converter); + } + + private static Optional getInputStream(DSLContext tx, String sql, UUID cardId, Function> converter) { + return tx.connectionResult(conn -> { + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, cardId); + + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + return Optional.empty(); + } + try (InputStream in = rs.getBinaryStream(1)) { + return converter.apply(in); + } + } + } + }); + } + public List listCards(DSLContext tx, UUID userId) { // TODO: V_USER_TEAMS SelectConditionStep> userTeams = tx.select(USER_TEAMS.TEAM_ID) @@ -108,29 +239,38 @@ public List listCards(DSLContext tx, UUID userId) { .from(TEAM_UI_PROCESS_CARDS) .where(TEAM_UI_PROCESS_CARDS.TEAM_ID.in(userTeams)); - SelectConditionStep> query = tx.select( - UI_PROCESS_CARDS.UI_PROCESS_CARD_ID, - PROJECTS.ORG_ID, - ORGANIZATIONS.ORG_NAME, - UI_PROCESS_CARDS.PROJECT_ID, - PROJECTS.PROJECT_NAME, - UI_PROCESS_CARDS.REPO_ID, - REPOSITORIES.REPO_NAME, - UI_PROCESS_CARDS.NAME, - UI_PROCESS_CARDS.ENTRY_POINT, - UI_PROCESS_CARDS.DESCRIPTION, - UI_PROCESS_CARDS.ICON) - .from(UI_PROCESS_CARDS) - .join(REPOSITORIES, JoinType.JOIN).on(REPOSITORIES.REPO_ID.eq(UI_PROCESS_CARDS.REPO_ID)) - .join(PROJECTS, JoinType.JOIN).on(PROJECTS.PROJECT_ID.eq(UI_PROCESS_CARDS.PROJECT_ID)) - .join(ORGANIZATIONS, JoinType.JOIN).on(ORGANIZATIONS.ORG_ID.eq(PROJECTS.ORG_ID)) + SelectConditionStep> query = + buildSelect(tx) .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.in(byUserFilter) .or(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.in(byTeamFilter))); return query.fetch(this::toEntry); } - private ProcessCardEntry toEntry(Record11 r) { + private static SelectOnConditionStep> buildSelect(DSLContext tx) { + + Field isCustomForm = when(field(UI_PROCESS_CARDS.FORM).isNotNull(), true).otherwise(false); + + return tx.select( + UI_PROCESS_CARDS.UI_PROCESS_CARD_ID, + PROJECTS.ORG_ID, + ORGANIZATIONS.ORG_NAME, + UI_PROCESS_CARDS.PROJECT_ID, + PROJECTS.PROJECT_NAME, + UI_PROCESS_CARDS.REPO_ID, + REPOSITORIES.REPO_NAME, + UI_PROCESS_CARDS.NAME, + UI_PROCESS_CARDS.ENTRY_POINT, + UI_PROCESS_CARDS.DESCRIPTION, + UI_PROCESS_CARDS.ICON, + isCustomForm.as("isCustomForm")) + .from(UI_PROCESS_CARDS) + .join(REPOSITORIES, JoinType.JOIN).on(REPOSITORIES.REPO_ID.eq(UI_PROCESS_CARDS.REPO_ID)) + .join(PROJECTS, JoinType.JOIN).on(PROJECTS.PROJECT_ID.eq(UI_PROCESS_CARDS.PROJECT_ID)) + .join(ORGANIZATIONS, JoinType.JOIN).on(ORGANIZATIONS.ORG_ID.eq(PROJECTS.ORG_ID)); + } + + private ProcessCardEntry toEntry(Record12 r) { return ProcessCardEntry.builder() .id(r.get(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)) .orgName(r.get(ORGANIZATIONS.ORG_NAME)) @@ -140,6 +280,7 @@ private ProcessCardEntry toEntry(Record11