{
- render() {
- const { items } = this.props;
- if (items === undefined) {
- return 'Loading';
- }
-
- if (items.length === 0) {
- return 'No processes found.';
- }
-
- return (
- {items.map((i) => renderCard(i.orgName, i.projects))}
- );
- }
-}
-
-export default UserProcessByOrgCards;
diff --git a/console2/src/components/molecules/UserProcessStats/index.tsx b/console2/src/components/molecules/UserProcessStats/index.tsx
deleted file mode 100644
index 3015ad37b6..0000000000
--- a/console2/src/components/molecules/UserProcessStats/index.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-/*-
- * *****
- * Concord
- * -----
- * Copyright (C) 2017 - 2018 Walmart Inc.
- * -----
- * Licensed 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 * as React from 'react';
-import { Link } from 'react-router-dom';
-import { getStatusSemanticColor, ProcessStatus } from '../../../api/process';
-import { queryParams } from '../../../api/common';
-import { useContext } from 'react';
-import { UserSessionContext } from '../../../session';
-
-export interface StatusCount {
- status: ProcessStatus;
- count: number;
-}
-
-interface Props {
- items: StatusCount[];
-}
-
-const renderItem = (initiator: string, status: ProcessStatus, count: number) => {
- return (
- 0 ? getStatusSemanticColor(status) : 'grey')}
- key={status}>
-
{count > 0 ? count : 0}
-
- {count > 0 ? (
- {status}
- ) : (
- status
- )}
-
-
- );
-};
-
-export default ({ items }: Props) => {
- const { userInfo } = useContext(UserSessionContext);
-
- return (
-
- {items.map((v) => renderItem(userInfo!.username, v.status, v.count))}
-
- );
-};
diff --git a/console2/src/components/molecules/index.tsx b/console2/src/components/molecules/index.tsx
index e57d93e639..2b786d38de 100644
--- a/console2/src/components/molecules/index.tsx
+++ b/console2/src/components/molecules/index.tsx
@@ -63,8 +63,6 @@ export { default as SingleOperationPopup } from './SingleOperationPopup';
export { default as TeamAccessDropdown } from './TeamAccessDropdown';
export { default as TeamAccessList } from './TeamAccessList';
export { default as TeamRoleDropdown } from './TeamRoleDropdown';
-export { default as UserProcessByOrgCards } from './UserProcessByOrgCards';
-export { default as UserProcessStats } from './UserProcessStats';
export { default as WithCopyToClipboard } from './WithCopyToClipboard';
// https://github.com/facebook/create-react-app/issues/6054
diff --git a/console2/src/components/organisms/UserProcessActivity/index.tsx b/console2/src/components/organisms/UserProcessActivity/index.tsx
index 058e8e1730..d6a4679a91 100644
--- a/console2/src/components/organisms/UserProcessActivity/index.tsx
+++ b/console2/src/components/organisms/UserProcessActivity/index.tsx
@@ -20,19 +20,13 @@
import * as React from 'react';
-import { ConcordKey } from '../../../api/common';
-import { UserProcessStats, UserProcessByOrgCards } from '../../molecules';
import {
getActivity as apiGetActivity,
- ProjectProcesses,
- UserActivity
+ listProcessCards as apiListProcessCards, ProcessCardEntry
} from '../../../api/service/console/user';
-import { Header } from 'semantic-ui-react';
-import { MAX_CARD_ITEMS, OrgProjects } from '../../molecules/UserProcessByOrgCards';
+import { Button, Card, CardGroup, Header, Icon, Image, Modal } from 'semantic-ui-react';
import { ProcessList } from '../../molecules/index';
-import { StatusCount } from '../../molecules/UserProcessStats';
-import { ProcessEntry, ProcessStatus } from '../../../api/process';
-import { comparators } from '../../../utils';
+import { ProcessEntry } from '../../../api/process';
import {
CREATED_AT_COLUMN,
INITIATOR_COLUMN,
@@ -45,6 +39,8 @@ import { useCallback, useEffect, useState } from 'react';
import { useApi } from '../../../hooks/useApi';
import { LoadingDispatch } from '../../../App';
import RequestErrorActivity from '../RequestErrorActivity';
+import {Link} from "react-router-dom";
+import './styles.css';
export interface ExternalProps {
forceRefresh: any;
@@ -52,33 +48,81 @@ export interface ExternalProps {
const MAX_OWN_PROCESSES = 10;
-const DEFAULT_STATS: StatusCount[] = [
- { status: ProcessStatus.ENQUEUED, count: -1 },
- { status: ProcessStatus.RUNNING, count: -1 },
- { status: ProcessStatus.SUSPENDED, count: -1 },
- { status: ProcessStatus.FINISHED, count: -1 },
- { status: ProcessStatus.FAILED, count: -1 }
-];
-
-const statusOrder = [
- ProcessStatus.RUNNING,
- ProcessStatus.SUSPENDED,
- ProcessStatus.FINISHED,
- ProcessStatus.FAILED
-];
+const renderCard = (card: ProcessCardEntry) => {
+ return (
+
+
+ {card.icon && }
+ {card.name}
+
+
+ Project: {card.projectName}
+
+
+ {card.description}
+
+
+
+
+ {card.isCustomForm &&
+
Start process}>
+
+
+
+
+
+ }
+
+ {!card.isCustomForm &&
+
Start process
+ }
+
+
+
+ );
+};
+
+const renderCards = (cards: ProcessCardEntry[]) => {
+ if (cards.length === 0) {
+ return;
+ }
+
+ return (
+
+ {cards.map((card) => renderCard(card))}
+
+ );
+};
+
+interface ActivityEntry {
+ processes?: ProcessEntry[];
+ cards?: ProcessCardEntry[];
+}
const UserProcesses = ({ forceRefresh }: ExternalProps) => {
const dispatch = React.useContext(LoadingDispatch);
- const [processStats, setProcessStats] = useState(DEFAULT_STATS);
- const [processByOrg, setProcessByOrg] = useState();
const [processes, setProcesses] = useState();
+ const [processCards, setProcessCards] = useState();
+ const [processCardsShow, toggleProcessCardsShow] = useState(true);
- const fetchData = useCallback(() => {
- return apiGetActivity(MAX_CARD_ITEMS + 1, MAX_OWN_PROCESSES);
+ const processCardsShowHandler = useCallback(() => {
+ toggleProcessCardsShow((prevState) => !prevState);
}, []);
- const { data, error } = useApi(fetchData, {
+ const fetchData = useCallback(async () => {
+ const activity = await apiGetActivity(MAX_OWN_PROCESSES);
+ const cards = await apiListProcessCards();
+ return { processes: activity.processes, cards}
+ }, []);
+
+ const { data, error } = useApi(fetchData, {
fetchOnMount: true,
forceRequest: forceRefresh,
dispatch: dispatch
@@ -89,27 +133,25 @@ const UserProcesses = ({ forceRefresh }: ExternalProps) => {
return;
}
- setProcessStats(makeProcessStatsList(data?.processStats));
- setProcessByOrg(makeProcessByOrgList(data?.orgProcesses));
- setProcesses(data?.processes);
+ setProcesses(data.processes);
+ setProcessCards(data.cards)
}, [data]);
return (
<>
{error && }
-
-
+ {processCards && processCards.length > 0 &&
+
+ }
-
-
+ {processCardsShow && processCards && renderCards(processCards)}
- Your last {MAX_OWN_PROCESSES} processes:
+ Your last {MAX_OWN_PROCESSES} processes
{
);
};
-const sort = (orgProcess: ProjectProcesses[]): ProjectProcesses[] =>
- orgProcess.sort(comparators.byProperty((i) => i.projectName));
-
-const makeProcessByOrgList = (orgProcesses?: {
- orgName: ConcordKey;
- processes: ProjectProcesses[];
-}): OrgProjects[] => {
- if (orgProcesses) {
- return Object.keys(orgProcesses)
- .map((k) => ({ orgName: k, projects: sort(orgProcesses[k]) }))
- .sort((a, b) => b.projects.length - a.projects.length);
- } else {
- return [];
- }
-};
-
-const makeProcessStatsList = (processStats?: {
- status: ProcessStatus;
- count: number;
-}): StatusCount[] => {
- if (processStats === undefined) {
- return DEFAULT_STATS;
- }
-
- return Object.keys(processStats)
- .map((k) => ({ status: k as ProcessStatus, count: processStats[k] }))
- .sort((a, b) => {
- const i1 = statusOrder.indexOf(a.status);
- const i2 = statusOrder.indexOf(b.status);
- return i1 - i2;
- });
-};
-
export default UserProcesses;
-//
-// const mapStateToProps = ({ userActivity }: { userActivity: State }): StateProps => ({
-// loading: userActivity.loading,
-// error: userActivity.error,
-// userActivity: userActivity.getUserActivity.response
-// ? userActivity.getUserActivity.response.activity
-// ? userActivity.getUserActivity.response.activity
-// : undefined
-// : undefined
-// });
-//
-// const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
-// load: () => dispatch(actions.getUserActivity(MAX_CARD_ITEMS + 1, MAX_OWN_PROCESSES))
-// });
diff --git a/console2/src/components/organisms/UserProcessActivity/styles.css b/console2/src/components/organisms/UserProcessActivity/styles.css
new file mode 100644
index 0000000000..8c16e6763f
--- /dev/null
+++ b/console2/src/components/organisms/UserProcessActivity/styles.css
@@ -0,0 +1,28 @@
+/*-
+ * *****
+ * Concord
+ * -----
+ * Copyright (C) 2017 - 2018 Walmart Inc.
+ * -----
+ * Licensed 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.
+ * =====
+ */
+.ui.cards>.card {
+ border: 1px solid #D4D4ED;
+ box-shadow: none;
+ border-radius: 0;
+}
+
+.no-margin-top {
+ margin-top: 0 !important;
+}
diff --git a/console2/src/components/pages/UserActivityPage/index.tsx b/console2/src/components/pages/UserActivityPage/index.tsx
index 91ea73ba6f..48bf74a59a 100644
--- a/console2/src/components/pages/UserActivityPage/index.tsx
+++ b/console2/src/components/pages/UserActivityPage/index.tsx
@@ -38,7 +38,7 @@ const UserActivityPage = () => {
return (
<>
- Activity today
+ Activity
diff --git a/examples/README.md b/examples/README.md
index acf689e7a3..b1d11daa09 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -52,6 +52,8 @@
* [imports](imports) - how to use external GIT/http/mvn resources as project files;
* [juel_java_steams](juel_java_steams) - using expressions, Groovy and Java Streams;
* [profiles](profiles) - how to use profiles;
+* [process_card_htmx](process_card_htmx) - how to use "process cards" and [HTMX](https://htmx.org/) to implement custom forms to start processes;
+* [process_card_jquery](process_card_jquery) - how to use "process cards" and [jQuery](https://jquery.com/) to implement custom forms to start processes;
* [python_script](python_script) - running a Python script from a flow;
* [ruby](ruby) - running a Ruby snippet from a flow;
* [script_url](script_url) - running an external script file;
diff --git a/examples/process_card_htmx/README.md b/examples/process_card_htmx/README.md
new file mode 100644
index 0000000000..6445a135a1
--- /dev/null
+++ b/examples/process_card_htmx/README.md
@@ -0,0 +1,34 @@
+# Process Cards
+
+"Process Cards" are a little tiles on the Activity page of the Concord UI.
+They can be used to start a predefined process with no parameters or to show a "form".
+The form is a static HTML page that will be displayed when user clicks on the card.
+The page can contain any HTML, CSS, and JavaScript code.
+
+This particular example demonstrates how to use [HTMX](https://htmx.org/) to
+implement a custom form to start a process.
+
+Steps:
+- create a new Concord project and register the Git repository with the flow
+ you want to run;
+- upload the form by calling the API (while in this directory):
+ ```
+ curl -i \
+ -H 'Authorization: yourApiKey' \
+ -F org=myOrg \
+ -F project=myProject \
+ -F repo=myRepo \
+ -F entryPoint=myFlow \
+ -F name=foobar \
+ -F description=Test \
+ -F form=@index.html \
+ http://localhost:8001/api/v1/processcard
+ ```
+- log into the Concord UI and check the Activity page:
+ ![Process Card](images/card.png)
+- click on "Start process" and you should see the form:
+ ![Form](images/form.png)
+
+The uploaded [index.html](index.html) file contains a simple form with a single input field.
+When the form is submitted, the `htmx:post` attribute triggers a POST request to the
+`/api/v1/process` endpoint with the form data.
diff --git a/examples/process_card_htmx/data.js b/examples/process_card_htmx/data.js
new file mode 100644
index 0000000000..a9687fa484
--- /dev/null
+++ b/examples/process_card_htmx/data.js
@@ -0,0 +1,10 @@
+data = {
+ "org": "Default",
+ "project": "test",
+ "repo": "test",
+ "entryPoint": "test",
+
+ "values" : {
+ "release" : "1.0.0"
+ }
+};
\ No newline at end of file
diff --git a/examples/process_card_htmx/images/card.png b/examples/process_card_htmx/images/card.png
new file mode 100644
index 0000000000..117206a6bc
Binary files /dev/null and b/examples/process_card_htmx/images/card.png differ
diff --git a/examples/process_card_htmx/images/form.png b/examples/process_card_htmx/images/form.png
new file mode 100644
index 0000000000..7a6fa3b9ab
Binary files /dev/null and b/examples/process_card_htmx/images/form.png differ
diff --git a/examples/process_card_htmx/index.html b/examples/process_card_htmx/index.html
new file mode 100644
index 0000000000..2ddbc6c3fe
--- /dev/null
+++ b/examples/process_card_htmx/index.html
@@ -0,0 +1,52 @@
+
+
+
+
+ Process start form example
+
+
+
+
+
+
+
+
+
+
+
+
+ The process has been started, see
the log for
+ details.
+
+
+
+
+
+
+
diff --git a/examples/process_card_jquery/README.md b/examples/process_card_jquery/README.md
new file mode 100644
index 0000000000..86ec6b4a9e
--- /dev/null
+++ b/examples/process_card_jquery/README.md
@@ -0,0 +1,3 @@
+# Process Cards
+
+See the description in the [process_card_htmx](../process_card_htmx) example.
diff --git a/examples/process_card_jquery/data.js b/examples/process_card_jquery/data.js
new file mode 100644
index 0000000000..a9687fa484
--- /dev/null
+++ b/examples/process_card_jquery/data.js
@@ -0,0 +1,10 @@
+data = {
+ "org": "Default",
+ "project": "test",
+ "repo": "test",
+ "entryPoint": "test",
+
+ "values" : {
+ "release" : "1.0.0"
+ }
+};
\ No newline at end of file
diff --git a/examples/process_card_jquery/index.html b/examples/process_card_jquery/index.html
new file mode 100644
index 0000000000..db53cbafb9
--- /dev/null
+++ b/examples/process_card_jquery/index.html
@@ -0,0 +1,159 @@
+
+
+
+
+ Process start form example
+
+
+
+
+
+
+
+
+
+ Deployment info
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/it/server/src/test/java/com/walmartlabs/concord/it/server/AbstractServerIT.java b/it/server/src/test/java/com/walmartlabs/concord/it/server/AbstractServerIT.java
index ca05a3e99d..addb11d36b 100644
--- a/it/server/src/test/java/com/walmartlabs/concord/it/server/AbstractServerIT.java
+++ b/it/server/src/test/java/com/walmartlabs/concord/it/server/AbstractServerIT.java
@@ -20,10 +20,7 @@
* =====
*/
-import com.walmartlabs.concord.client2.ApiClient;
-import com.walmartlabs.concord.client2.ApiException;
-import com.walmartlabs.concord.client2.SecretOperationResponse;
-import com.walmartlabs.concord.client2.StartProcessResponse;
+import com.walmartlabs.concord.client2.*;
import com.walmartlabs.concord.common.IOUtils;
import com.walmartlabs.concord.it.common.ITUtils;
import com.walmartlabs.concord.it.common.JGitUtils;
@@ -221,4 +218,33 @@ protected static String env(String k, String def) {
public static boolean shouldSkipDockerTests() {
return Boolean.parseBoolean(System.getenv("IT_SKIP_DOCKER_TESTS"));
}
+
+ protected void withOrg(Consumer consumer) throws Exception {
+ String orgName = "org_" + randomString();
+ OrganizationsApi orgApi = new OrganizationsApi(getApiClient());
+ try {
+ orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));
+ consumer.accept(orgName);
+ } finally {
+ orgApi.deleteOrg(orgName, "yes");
+ }
+ }
+
+ protected void withProject(String orgName, Consumer consumer) throws Exception {
+ String projectName = "project_" + randomString();
+ ProjectsApi projectsApi = new ProjectsApi(getApiClient());
+ try {
+ projectsApi.createOrUpdateProject(orgName, new ProjectEntry()
+ .name(projectName)
+ .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));
+ consumer.accept(projectName);
+ } finally {
+ projectsApi.deleteProject(orgName, projectName);
+ }
+ }
+
+ @FunctionalInterface
+ public interface Consumer {
+ void accept(T t) throws Exception;
+ }
}
diff --git a/it/server/src/test/java/com/walmartlabs/concord/it/server/JsonStoreTaskIT.java b/it/server/src/test/java/com/walmartlabs/concord/it/server/JsonStoreTaskIT.java
index 4de663702e..a6526de43b 100644
--- a/it/server/src/test/java/com/walmartlabs/concord/it/server/JsonStoreTaskIT.java
+++ b/it/server/src/test/java/com/walmartlabs/concord/it/server/JsonStoreTaskIT.java
@@ -90,30 +90,6 @@ public void testPutGetData() throws Exception {
}
- private void withOrg(Consumer consumer) throws Exception {
- String orgName = "org_" + randomString();
- OrganizationsApi orgApi = new OrganizationsApi(getApiClient());
- try {
- orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));
- consumer.accept(orgName);
- } finally {
- orgApi.deleteOrg(orgName, "yes");
- }
- }
-
- private void withProject(String orgName, Consumer consumer) throws Exception {
- String projectName = "project_" + randomString();
- ProjectsApi projectsApi = new ProjectsApi(getApiClient());
- try {
- projectsApi.createOrUpdateProject(orgName, new ProjectEntry()
- .name(projectName)
- .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));
- consumer.accept(projectName);
- } finally {
- projectsApi.deleteProject(orgName, projectName);
- }
- }
-
private void withStore(String orgName, Consumer consumer) throws Exception {
String storageName = "storage_" + randomString();
JsonStoreApi storageApi = new JsonStoreApi(getApiClient());
@@ -126,9 +102,4 @@ private void withStore(String orgName, Consumer consumer) throws Excepti
storageApi.deleteJsonStore(orgName, storageName);
}
}
-
- @FunctionalInterface
- public interface Consumer {
- void accept(T t) throws Exception;
- }
}
diff --git a/it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessCardIT.java b/it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessCardIT.java
new file mode 100644
index 0000000000..802db8c410
--- /dev/null
+++ b/it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessCardIT.java
@@ -0,0 +1,134 @@
+package com.walmartlabs.concord.it.server;
+
+/*-
+ * *****
+ * Concord
+ * -----
+ * Copyright (C) 2017 - 2024 Walmart Inc.
+ * -----
+ * Licensed 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 com.walmartlabs.concord.client2.ApiException;
+import com.walmartlabs.concord.client2.ProcessCardAccessEntry;
+import com.walmartlabs.concord.client2.ProcessCardEntry;
+import com.walmartlabs.concord.client2.ProcessCardsApi;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ProcessCardIT extends AbstractServerIT {
+
+ @Test
+ public void testInvalidRequests() throws Exception {
+ ProcessCardsApi api = new ProcessCardsApi(getApiClient());
+
+ // no cards
+ List cards = api.listUserProcessCards();
+ assertNotNull(cards);
+ assertTrue(cards.isEmpty());
+
+ // unknown card id -> 404 for form
+ try {
+ api.getProcessCardForm(UUID.randomUUID());
+ fail("exception expected");
+ } catch (ApiException e) {
+ assertEquals(404, e.getCode());
+ }
+
+ // unknown card id -> 404 for form data
+ try {
+ api.getProcessCardFormData(UUID.randomUUID());
+ fail("exception expected");
+ } catch (ApiException e) {
+ assertEquals(404, e.getCode());
+ }
+
+ // unknown card id -> 404 for delete card
+ try {
+ api.deleteProcessCard(UUID.randomUUID());
+ fail("exception expected");
+ } catch (ApiException e) {
+ assertEquals(404, e.getCode());
+ }
+ }
+
+ @Test
+ public void testCRUD() throws Exception {
+ ProcessCardsApi api = new ProcessCardsApi(getApiClient());
+
+ withOrg(orgName -> {
+ withProject(orgName, projectName -> {
+ var r = new ProcessCardsApi.CreateOrUpdateProcessCardRequest()
+ .org(orgName)
+ .project(projectName)
+ .name("test")
+ .description("test description")
+ .data(Map.of("myKey", "myValue"))
+ .form("hello world")
+ .icon("test icon");
+
+ // create card
+ UUID cardId = api.createOrUpdateProcessCard(r.asMap()).getId();
+
+ ProcessCardEntry entry = api.getProcessCard(cardId);
+ assertEquals(orgName, entry.getOrgName());
+ assertEquals(projectName, entry.getProjectName());
+ assertEquals("test", entry.getName());
+ assertEquals("test description", entry.getDescription());
+
+ // -- list cards
+ List cards = api.listUserProcessCards();
+ assertEquals(1, cards.size());
+
+ try (InputStream is = api.getProcessCardForm(cardId)) {
+ assertEquals("hello world", new String(is.readAllBytes(), StandardCharsets.UTF_8));
+ }
+
+ try (InputStream is = api.getProcessCardFormData(cardId)) {
+ String data = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ assertTrue(data.startsWith("data = "), data);
+ assertTrue(data.contains("myKey"), data);
+ assertTrue(data.contains("myValue"), data);
+ }
+
+ // update card
+ r = new ProcessCardsApi.CreateOrUpdateProcessCardRequest()
+ .id(cardId)
+ .org(orgName)
+ .project(projectName)
+ .name("test update")
+ .description("test description update");
+
+ UUID cardIdAfterUpdate = api.createOrUpdateProcessCard(r.asMap()).getId();
+ assertEquals(cardId, cardIdAfterUpdate);
+
+ entry = api.getProcessCard(cardId);
+ assertEquals(orgName, entry.getOrgName());
+ assertEquals(projectName, entry.getProjectName());
+ assertEquals("test update", entry.getName());
+ assertEquals("test description update", entry.getDescription());
+
+ // delete card
+ api.deleteProcessCard(cardId);
+ });
+ });
+ }
+}
diff --git a/server/db/src/main/resources/com/walmartlabs/concord/server/db/liquibase.xml b/server/db/src/main/resources/com/walmartlabs/concord/server/db/liquibase.xml
index fa344fa1ee..d9a61a4af6 100644
--- a/server/db/src/main/resources/com/walmartlabs/concord/server/db/liquibase.xml
+++ b/server/db/src/main/resources/com/walmartlabs/concord/server/db/liquibase.xml
@@ -112,5 +112,6 @@
+
diff --git a/server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.10.0.xml b/server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.10.0.xml
new file mode 100644
index 0000000000..a06d93fc23
--- /dev/null
+++ b/server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.10.0.xml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/MultipartUtils.java b/server/impl/src/main/java/com/walmartlabs/concord/server/MultipartUtils.java
index de6e2bd3a9..335cc86c5e 100644
--- a/server/impl/src/main/java/com/walmartlabs/concord/server/MultipartUtils.java
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/MultipartUtils.java
@@ -229,9 +229,9 @@ public static List getStringList(MultipartInput input, String key) {
}
}
public static List getUUIDList(MultipartInput input, String key) {
- List projects = getStringList(input,key);
+ List result = getStringList(input, key);
try {
- return projects.stream().map(UUID::fromString).collect(Collectors.toList());
+ return result.stream().map(UUID::fromString).collect(Collectors.toList());
} catch (IllegalArgumentException e) {
throw new ConcordApplicationException("Error parsing the request", e);
}
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/ConsoleModule.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ConsoleModule.java
index c261e7c74d..fd1965c157 100644
--- a/server/impl/src/main/java/com/walmartlabs/concord/server/console/ConsoleModule.java
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ConsoleModule.java
@@ -36,7 +36,7 @@ public void configure(Binder binder) {
binder.bind(CustomFormServiceV1.class).in(SINGLETON);
binder.bind(CustomFormServiceV2.class).in(SINGLETON);
- bindJaxRsResource(binder, UserActivityResource.class);
+ bindJaxRsResource(binder, UserActivityResourceV2.class);
bindJaxRsResource(binder, CustomFormService.class);
bindJaxRsResource(binder, ConsoleService.class);
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV1.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV1.java
index 4f611bdc76..fd82eb9c9d 100644
--- a/server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV1.java
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV1.java
@@ -24,6 +24,8 @@
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.walmartlabs.concord.forms.ValidationError;
import com.walmartlabs.concord.server.MultipartUtils;
import com.walmartlabs.concord.server.cfg.CustomFormConfiguration;
@@ -44,10 +46,12 @@
import io.takari.bpm.model.form.FormDefinition;
import io.takari.bpm.model.form.FormField;
import io.takari.bpm.model.form.FormField.Cardinality;
+import org.immutables.value.Value;
import org.jboss.resteasy.plugins.providers.multipart.MultipartInput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
@@ -334,7 +338,17 @@ private FormData prepareData(boolean success, boolean processFailed, Form form,
}
}
- return new FormData(success, processFailed, submitUrl, fields, _definitions, _values, _errors);
+ return FormData.builder()
+ .txId(processInstanceId)
+ .formName(formName)
+ .success(success)
+ .processFailed(processFailed)
+ .submitUrl(submitUrl)
+ .fields(fields)
+ .definitions(_definitions)
+ .values(_values)
+ .errors(_errors)
+ .build();
}
private void writeData(Path baseDir, Object data) throws IOException {
@@ -396,56 +410,36 @@ private static String formPath(PartialProcessKey processKey, String formName) {
return String.format(FORMS_PATH_TEMPLATE, processKey, formName);
}
- @JsonInclude(Include.NON_EMPTY)
- public static class FormData implements Serializable {
-
- private static final long serialVersionUID = -1591440785695774602L;
-
- private final boolean success;
- private final boolean processFailed;
- private final String submitUrl;
- private final List fields;
- private final Map definitions;
- private final Map values;
- private final Map errors;
-
- public FormData(boolean success, boolean processFailed, String submitUrl,
- List fields, Map definitions, Map values,
- Map errors) {
-
- this.success = success;
- this.processFailed = processFailed;
- this.submitUrl = submitUrl;
- this.fields = fields;
- this.definitions = definitions;
- this.values = values;
- this.errors = errors;
- }
+ @Value.Immutable
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @JsonSerialize(as = ImmutableFormData.class)
+ @JsonDeserialize(as = ImmutableFormData.class)
+ public interface FormData extends Serializable {
- public boolean isSuccess() {
- return success;
- }
+ long serialVersionUID = -1591440785695774602L;
- public boolean isProcessFailed() {
- return processFailed;
- }
+ String txId();
- public String getSubmitUrl() {
- return submitUrl;
- }
+ String formName();
- public List getFields() { return fields; }
+ boolean success();
- public Map getDefinitions() {
- return definitions;
- }
+ boolean processFailed();
- public Map getValues() {
- return values;
- }
+ String submitUrl();
+
+ List fields();
+
+ Map definitions();
+
+ @Nullable
+ Map values();
+
+ @Nullable
+ Map errors();
- public Map getErrors() {
- return errors;
+ static ImmutableFormData.Builder builder() {
+ return ImmutableFormData.builder();
}
}
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV2.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV2.java
index fec05f66c8..eb51563fb1 100644
--- a/server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV2.java
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV2.java
@@ -315,7 +315,18 @@ private FormData prepareData(boolean success, boolean processFailed, Form form,
}
String submitUrl = String.format(FORM_WIZARD_CONTINUE_URL_TEMPLATE, processInstanceId, form.name());
- return new FormData(success, processFailed, submitUrl, fields, _definitions, _values, _errors);
+
+ return FormData.builder()
+ .txId(processInstanceId.toString())
+ .formName(form.name())
+ .success(success)
+ .processFailed(processFailed)
+ .submitUrl(submitUrl)
+ .fields(fields)
+ .definitions(_definitions)
+ .values(_values)
+ .errors(_errors)
+ .build();
}
private io.takari.bpm.model.form.FormField.Cardinality convert(FormField.Cardinality c) {
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardAccessEntry.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardAccessEntry.java
new file mode 100644
index 0000000000..073520827c
--- /dev/null
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardAccessEntry.java
@@ -0,0 +1,49 @@
+package com.walmartlabs.concord.server.console;
+
+/*-
+ * *****
+ * Concord
+ * -----
+ * Copyright (C) 2017 - 2023 Walmart Inc.
+ * -----
+ * Licensed 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 com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.immutables.value.Value;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.UUID;
+
+@Value.Immutable
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+@JsonSerialize(as = ImmutableProcessCardAccessEntry.class)
+@JsonDeserialize(as = ImmutableProcessCardAccessEntry.class)
+public interface ProcessCardAccessEntry extends Serializable {
+
+ long serialVersionUID = 1L;
+
+ @Value.Default
+ default List userIds() {
+ return List.of();
+ }
+
+ @Value.Default
+ default List teamIds() {
+ return List.of();
+ }
+}
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
new file mode 100644
index 0000000000..a4f1092772
--- /dev/null
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardEntry.java
@@ -0,0 +1,76 @@
+package com.walmartlabs.concord.server.console;
+
+/*-
+ * *****
+ * Concord
+ * -----
+ * Copyright (C) 2017 - 2023 Walmart Inc.
+ * -----
+ * Licensed 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 com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.walmartlabs.concord.common.validation.ConcordKey;
+import com.walmartlabs.concord.server.org.ImmutableEntityOwner;
+import org.immutables.value.Value;
+
+import javax.annotation.Nullable;
+import javax.validation.constraints.Size;
+import java.io.Serializable;
+import java.util.UUID;
+
+@Value.Immutable
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+@JsonSerialize(as = ImmutableProcessCardEntry.class)
+@JsonDeserialize(as = ImmutableProcessCardEntry.class)
+public interface ProcessCardEntry extends Serializable {
+
+ long serialVersionUID = 1L;
+
+ UUID id();
+
+ @ConcordKey
+ @Nullable
+ String orgName();
+
+ @ConcordKey
+ @Nullable
+ String projectName();
+
+ @ConcordKey
+ @Nullable
+ String repoName();
+
+ @Size(max = 256)
+ @Nullable
+ String entryPoint();
+
+ @Size(max = 128)
+ String name();
+
+ @Size(max = 512)
+ @Nullable
+ String description();
+
+ @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/ProcessCardManager.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardManager.java
new file mode 100644
index 0000000000..452988bbbe
--- /dev/null
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardManager.java
@@ -0,0 +1,460 @@
+package com.walmartlabs.concord.server.console;
+
+/*-
+ * *****
+ * Concord
+ * -----
+ * Copyright (C) 2017 - 2024 Walmart Inc.
+ * -----
+ * Licensed 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 com.walmartlabs.concord.db.AbstractDao;
+import com.walmartlabs.concord.db.MainDB;
+import com.walmartlabs.concord.server.ConcordObjectMapper;
+import com.walmartlabs.concord.server.OperationResult;
+import com.walmartlabs.concord.server.jooq.tables.UiProcessCards;
+import com.walmartlabs.concord.server.jooq.tables.Users;
+import com.walmartlabs.concord.server.jooq.tables.records.UiProcessCardsRecord;
+import com.walmartlabs.concord.server.org.EntityOwner;
+import com.walmartlabs.concord.server.org.ResourceAccessUtils;
+import com.walmartlabs.concord.server.security.Roles;
+import com.walmartlabs.concord.server.security.UserPrincipal;
+import com.walmartlabs.concord.server.user.UserType;
+import org.apache.shiro.authz.UnauthorizedException;
+import org.jooq.*;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.InputStream;
+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 com.walmartlabs.concord.server.jooq.tables.Users.USERS;
+import static org.jooq.impl.DSL.*;
+
+@Named
+public class ProcessCardManager {
+
+ private final Dao dao;
+
+ @Inject
+ public ProcessCardManager(Dao dao) {
+ this.dao = dao;
+ }
+
+ public ProcessCardEntry get(UUID cardId) {
+ return dao.get(cardId);
+ }
+
+ public void delete(UUID cardId) {
+ dao.delete(cardId);
+ }
+
+ public void assertAccess(UUID cardId) {
+ if (Roles.isAdmin()) {
+ return;
+ }
+
+ UserPrincipal principal = UserPrincipal.assertCurrent();
+
+ EntityOwner owner = dao.getOwner(cardId);
+ if (ResourceAccessUtils.isSame(principal, owner)) {
+ return;
+ }
+
+ throw new UnauthorizedException("The current user (" + principal.getUsername() + ") doesn't have " +
+ "access to the process card: " + cardId);
+ }
+
+ public void updateAccess(UUID cardId, List teamIds, List userIds) {
+ dao.tx(tx -> {
+ dao.rewriteTeamAccess(tx, cardId, teamIds);
+ dao.rewriteUserAccess(tx, cardId, userIds);
+ });
+ }
+
+ public List listUserCards(UUID userId) {
+ return dao.listCards(userId);
+ }
+
+ public ProcessCardOperationResponse createOrUpdate(UUID id, UUID projectId, UUID repoId, String name, String entryPoint, String description, InputStream icon, InputStream form, Map data) {
+ boolean exists = false;
+ if (id != null) {
+ exists = dao.exists(id);
+ }
+
+ if (!exists) {
+ UUID resultId = dao.insert(id, projectId, repoId, name, entryPoint, description, icon, form, data);
+ return new ProcessCardOperationResponse(resultId, OperationResult.CREATED);
+ } else {
+ assertAccess(id);
+
+ dao.update(id, projectId, repoId, name, entryPoint, description, icon, form, data);
+ return new ProcessCardOperationResponse(id, OperationResult.UPDATED);
+ }
+ }
+
+ public Optional getForm(UUID cardId, Function> converter) {
+ return dao.getForm(cardId, converter);
+ }
+
+ public Map getFormData(UUID cardId) {
+ return dao.getFormData(cardId);
+ }
+
+ public static class Dao extends AbstractDao {
+
+ private final ConcordObjectMapper objectMapper;
+
+ @Inject
+ protected Dao(@MainDB Configuration cfg, ConcordObjectMapper objectMapper) {
+ super(cfg);
+ this.objectMapper = objectMapper;
+ }
+
+ protected void tx(Tx t) {
+ super.tx(t);
+ }
+
+ public ProcessCardEntry get(UUID cardId) {
+ return txResult(tx -> get(tx, cardId));
+ }
+
+ private ProcessCardEntry get(DSLContext tx, UUID cardId) {
+ return buildSelect(tx)
+ .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId))
+ .fetchOne(this::toEntry);
+ }
+
+ public UUID insert(UUID cardId, UUID projectId, UUID repoId, String name, String entryPoint, String description, InputStream icon, InputStream form, Map data) {
+ return txResult(tx -> insert(tx, cardId, projectId, repoId, name, entryPoint, description, icon, form, data));
+ }
+
+ private UUID insert(DSLContext tx, UUID cardId, UUID projectId, UUID repoId, String name, String entryPoint, String description, InputStream icon, InputStream form, Map data) {
+ String sql = tx.insertInto(UI_PROCESS_CARDS)
+ .columns(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID,
+ UI_PROCESS_CARDS.PROJECT_ID,
+ UI_PROCESS_CARDS.REPO_ID,
+ UI_PROCESS_CARDS.NAME,
+ UI_PROCESS_CARDS.ENTRY_POINT,
+ UI_PROCESS_CARDS.DESCRIPTION,
+ UI_PROCESS_CARDS.ICON,
+ UI_PROCESS_CARDS.FORM,
+ UI_PROCESS_CARDS.DATA,
+ UI_PROCESS_CARDS.OWNER_ID)
+ .values((UUID)null, null, null, null, null, null, null, null, null, null)
+ .returning(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)
+ .getSQL();
+
+ return tx.connectionResult(conn -> {
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setObject(1, cardId == null ? UUID.randomUUID() : cardId);
+ ps.setObject(2, projectId);
+ ps.setObject(3, repoId);
+ ps.setString(4, name);
+ ps.setString(5, entryPoint);
+ ps.setString(6, description);
+ ps.setBinaryStream(7, icon);
+ ps.setBinaryStream(8, form);
+ ps.setObject(9, data != null ? objectMapper.toJSONB(data).data() : null);
+ ps.setObject(10, UserPrincipal.assertCurrent().getId());
+
+ try (ResultSet rs = ps.executeQuery()) {
+ if (!rs.next()) {
+ throw new RuntimeException("Can't insert process card");
+ }
+
+ return UUID.fromString(rs.getString(1));
+ }
+ }
+ });
+ }
+
+ public void update(UUID cardId, UUID projectId, UUID repoId, String name,
+ String entryPoint, String description, InputStream icon, InputStream form,
+ Map data) {
+ tx(tx -> update(tx, cardId, projectId, repoId, name, entryPoint, description, icon, form, data));
+ }
+
+ public void update(DSLContext tx, UUID cardId, UUID projectId, UUID repoId, String name,
+ String entryPoint, String description, InputStream icon, InputStream form,
+ Map data) {
+
+ List params = new ArrayList<>();
+ UpdateSetStep q = tx.update(UI_PROCESS_CARDS);
+
+ if (projectId != null) {
+ q.set(UI_PROCESS_CARDS.PROJECT_ID, (UUID)null);
+ params.add(projectId);
+ }
+
+ if (repoId != null) {
+ q.set(UI_PROCESS_CARDS.REPO_ID, (UUID)null);
+ params.add(repoId);
+ }
+
+ if (name != null) {
+ q.set(UI_PROCESS_CARDS.NAME, (String)null);
+ params.add(name);
+ }
+
+ if (entryPoint != null) {
+ q.set(UI_PROCESS_CARDS.ENTRY_POINT, (String)null);
+ params.add(entryPoint);
+ }
+
+ if (description != null) {
+ q.set(UI_PROCESS_CARDS.DESCRIPTION, (String)null);
+ params.add(description);
+ }
+
+ if (icon != null) {
+ q.set(UI_PROCESS_CARDS.ICON, (byte[]) null);
+ params.add(icon);
+ }
+
+ if (form != null) {
+ q.set(UI_PROCESS_CARDS.FORM, (byte[])null);
+ params.add(form);
+ }
+
+ if (data != null) {
+ q.set(UI_PROCESS_CARDS.DATA, (JSONB)null);
+ params.add(objectMapper.toJSONB(data).data());
+ }
+
+ if (params.isEmpty()) {
+ return;
+ }
+
+ String sql = q.set(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID, cardId)
+ .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId))
+ .getSQL();
+ params.add(cardId);
+ params.add(cardId);
+
+ tx.connection(conn -> {
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (int i = 0; i < params.size(); i++) {
+ Object p = params.get(i);
+ if (p instanceof InputStream) {
+ ps.setBinaryStream(i + 1, (InputStream) p);
+ } else {
+ ps.setObject(i + 1, p);
+ }
+ }
+
+ ps.executeUpdate();
+ }
+ });
+ }
+
+ public void delete(UUID id) {
+ tx(tx -> tx.delete(UI_PROCESS_CARDS)
+ .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(id))
+ .execute());
+ }
+
+ public boolean exists(UUID id) {
+ return txResult(tx -> tx.select(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)
+ .from(UI_PROCESS_CARDS)
+ .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(id))
+ .fetchOne(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)) != null;
+ }
+
+ public void rewriteTeamAccess(DSLContext tx, UUID cardId, List teamIds) {
+ tx.delete(TEAM_UI_PROCESS_CARDS)
+ .where(TEAM_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId))
+ .execute();
+
+ for (UUID teamId : teamIds) {
+ tx.insertInto(TEAM_UI_PROCESS_CARDS)
+ .columns(TEAM_UI_PROCESS_CARDS.TEAM_ID, TEAM_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)
+ .values(teamId, cardId)
+ .execute();
+ }
+ }
+
+ public void rewriteUserAccess(DSLContext tx, UUID cardId, List userIds) {
+ tx.delete(USER_UI_PROCESS_CARDS)
+ .where(USER_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId))
+ .execute();
+
+ for (UUID userId : userIds) {
+ tx.insertInto(USER_UI_PROCESS_CARDS)
+ .columns(USER_UI_PROCESS_CARDS.USER_ID, USER_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)
+ .values(userId, cardId)
+ .execute();
+ }
+ }
+
+ public EntityOwner getOwner(UUID cardId) {
+ Users u = USERS.as("u");
+ UiProcessCards c = UI_PROCESS_CARDS.as("c");
+
+ return txResult(tx -> select(
+ c.OWNER_ID,
+ u.USER_ID,
+ u.USERNAME,
+ u.DOMAIN,
+ u.DISPLAY_NAME,
+ u.USER_TYPE)
+ .from(c)
+ .leftJoin(u).on(u.USER_ID.eq(c.OWNER_ID))
+ .where(c.UI_PROCESS_CARD_ID.eq(cardId))
+ .fetchOne(r -> toOwner(r.get(c.OWNER_ID), r.get(u.USERNAME), r.get(u.DOMAIN), r.get(u.DISPLAY_NAME), r.get(u.USER_TYPE))));
+ }
+
+ public List listCards(UUID userId) {
+ return txResult(tx -> listCards(tx, userId));
+ }
+
+ private List listCards(DSLContext tx, UUID userId) {
+ var userTeams = tx.select(V_USER_TEAMS.TEAM_ID)
+ .from(V_USER_TEAMS)
+ .where(V_USER_TEAMS.USER_ID.eq(userId));
+
+ var byUserFilter = tx.select(USER_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)
+ .from(USER_UI_PROCESS_CARDS)
+ .where(USER_UI_PROCESS_CARDS.USER_ID.eq(userId));
+
+ var byTeamFilter = tx.select(TEAM_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)
+ .from(TEAM_UI_PROCESS_CARDS)
+ .where(TEAM_UI_PROCESS_CARDS.TEAM_ID.in(userTeams));
+
+ var byOwnerFilter = tx.select(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)
+ .from(UI_PROCESS_CARDS)
+ .where(UI_PROCESS_CARDS.OWNER_ID.isNotNull().and(UI_PROCESS_CARDS.OWNER_ID.eq(userId)));
+
+ var userCards = byUserFilter
+ .unionAll(byTeamFilter)
+ .unionAll(byOwnerFilter);
+
+ var userCardsFilter = tx.select(userCards.field(TEAM_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID))
+ .from(userCards);
+
+ var query = buildSelect(tx)
+ .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.in(userCardsFilter));
+
+ return query
+ .orderBy(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)
+ .fetch(this::toEntry);
+ }
+
+ 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);
+ }
+
+ 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)));
+ }
+
+ 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);
+ }
+ }
+ }
+ });
+ }
+
+ 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)
+ .leftJoin(REPOSITORIES).on(REPOSITORIES.REPO_ID.eq(UI_PROCESS_CARDS.REPO_ID))
+ .leftJoin(PROJECTS).on(PROJECTS.PROJECT_ID.eq(UI_PROCESS_CARDS.PROJECT_ID))
+ .leftJoin(ORGANIZATIONS).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))
+ .projectName(r.get(PROJECTS.PROJECT_NAME))
+ .repoName(r.get(REPOSITORIES.REPO_NAME))
+ .entryPoint(r.get(UI_PROCESS_CARDS.ENTRY_POINT))
+ .name(r.get(UI_PROCESS_CARDS.NAME))
+ .description(r.get(UI_PROCESS_CARDS.DESCRIPTION))
+ .icon(encodeBase64(r.get(UI_PROCESS_CARDS.ICON)))
+ .isCustomForm(r.get("isCustomForm", Boolean.class))
+ .build();
+ }
+
+ static String encodeBase64(byte[] value) {
+ if (value == null) {
+ return null;
+ }
+
+ return Base64.getEncoder().encodeToString(value);
+ }
+
+ private static EntityOwner toOwner(UUID id, String username, String domain, String displayName, String userType) {
+ if (id == null) {
+ return null;
+ }
+ return EntityOwner.builder()
+ .id(id)
+ .username(username)
+ .userDomain(domain)
+ .displayName(displayName)
+ .userType(UserType.valueOf(userType))
+ .build();
+ }
+ }
+}
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardOperationResponse.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardOperationResponse.java
new file mode 100644
index 0000000000..9e7b5982d0
--- /dev/null
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardOperationResponse.java
@@ -0,0 +1,65 @@
+package com.walmartlabs.concord.server.console;
+
+/*-
+ * *****
+ * Concord
+ * -----
+ * Copyright (C) 2017 - 2018 Walmart Inc.
+ * -----
+ * Licensed 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 com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.walmartlabs.concord.server.OperationResult;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public class ProcessCardOperationResponse implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final boolean ok = true;
+ private final UUID id;
+ private final OperationResult result;
+
+ @JsonCreator
+ public ProcessCardOperationResponse(@JsonProperty("id") UUID id,
+ @JsonProperty("result") OperationResult result) {
+ this.id = id;
+ this.result = result;
+ }
+
+ public boolean isOk() {
+ return ok;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public OperationResult getResult() {
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ProcessCardOperationResponse{" +
+ "ok=" + ok +
+ ", id=" + id +
+ ", result=" + result +
+ '}';
+ }
+}
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardRequest.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardRequest.java
new file mode 100644
index 0000000000..0f823eb001
--- /dev/null
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardRequest.java
@@ -0,0 +1,108 @@
+package com.walmartlabs.concord.server.console;
+
+/*-
+ * *****
+ * Concord
+ * -----
+ * Copyright (C) 2017 - 2023 Walmart Inc.
+ * -----
+ * Licensed 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 com.walmartlabs.concord.sdk.Constants;
+import com.walmartlabs.concord.server.MultipartUtils;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.jboss.resteasy.plugins.providers.multipart.MultipartInput;
+
+import java.io.InputStream;
+import java.util.Map;
+import java.util.UUID;
+
+public class ProcessCardRequest {
+
+ public static ProcessCardRequest from(MultipartInput input) {
+ return new ProcessCardRequest(input);
+ }
+
+ private final MultipartInput input;
+
+ private ProcessCardRequest(MultipartInput input) {
+ this.input = input;
+ }
+
+ @Schema(name = Constants.Multipart.ORG_ID)
+ public UUID getOrgId() {
+ return MultipartUtils.getUuid(input, Constants.Multipart.ORG_ID);
+ }
+
+ @Schema(name = Constants.Multipart.ORG_NAME)
+ public String getOrgName() {
+ return MultipartUtils.getString(input, Constants.Multipart.ORG_NAME);
+ }
+
+ @Schema(name = Constants.Multipart.PROJECT_ID)
+ public UUID getProjectId() {
+ return MultipartUtils.getUuid(input, Constants.Multipart.PROJECT_ID);
+ }
+
+ @Schema(name = Constants.Multipart.PROJECT_NAME)
+ public String getProjectName() {
+ return MultipartUtils.getString(input, Constants.Multipart.PROJECT_NAME);
+ }
+
+ @Schema(name = Constants.Multipart.REPO_ID)
+ public UUID getRepoId() {
+ return MultipartUtils.getUuid(input, Constants.Multipart.REPO_ID);
+ }
+
+ @Schema(name = Constants.Multipart.REPO_NAME)
+ public String getRepoName() {
+ return MultipartUtils.getString(input, Constants.Multipart.REPO_NAME);
+ }
+
+ @Schema(name = Constants.Multipart.ENTRY_POINT)
+ public String getEntryPoint() {
+ return MultipartUtils.getString(input, Constants.Multipart.ENTRY_POINT);
+ }
+
+ @Schema(name = "name")
+ public String getName() {
+ return MultipartUtils.getString(input, "name");
+ }
+
+ @Schema(name = "description")
+ public String getDescription() {
+ return MultipartUtils.getString(input, "description");
+ }
+
+ @Schema(name = "data")
+ public Map getData() {
+ return MultipartUtils.getMap(input, "data");
+ }
+
+ @Schema(name = "id")
+ public UUID getId() {
+ return MultipartUtils.getUuid(input, "id");
+ }
+
+ @Schema(name = "icon")
+ public InputStream getIcon() {
+ return MultipartUtils.getStream(input, "icon");
+ }
+
+ @Schema(name = "form")
+ public InputStream getForm() {
+ return MultipartUtils.getStream(input, "form");
+ }
+}
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardResource.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardResource.java
new file mode 100644
index 0000000000..693bc40c25
--- /dev/null
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardResource.java
@@ -0,0 +1,263 @@
+package com.walmartlabs.concord.server.console;
+
+/*-
+ * *****
+ * Concord
+ * -----
+ * Copyright (C) 2017 - 2023 Walmart Inc.
+ * -----
+ * Licensed 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 com.walmartlabs.concord.common.IOUtils;
+import com.walmartlabs.concord.server.ConcordObjectMapper;
+import com.walmartlabs.concord.server.GenericOperationResult;
+import com.walmartlabs.concord.server.OperationResult;
+import com.walmartlabs.concord.server.org.OrganizationManager;
+import com.walmartlabs.concord.server.org.project.ProjectDao;
+import com.walmartlabs.concord.server.org.project.RepositoryDao;
+import com.walmartlabs.concord.server.sdk.ConcordApplicationException;
+import com.walmartlabs.concord.server.sdk.metrics.WithTimer;
+import com.walmartlabs.concord.server.sdk.rest.Resource;
+import com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;
+import com.walmartlabs.concord.server.security.UserPrincipal;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.jboss.resteasy.plugins.providers.multipart.MultipartInput;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+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.StandardCopyOption;
+import java.util.*;
+
+@Named
+@Singleton
+@javax.ws.rs.Path("/api/v1/")
+@Tag(name = "ProcessCards")
+public class ProcessCardResource implements Resource {
+
+ private static final String DATA_FILE_TEMPLATE = "data = %s;";
+
+ private final ProcessCardManager processCardManager;
+ private final OrganizationManager organizationManager;
+ private final ProjectDao projectDao;
+ private final RepositoryDao repositoryDao;
+ private final ConcordObjectMapper objectMapper;
+
+ @Inject
+ public ProcessCardResource(OrganizationManager organizationManager,
+ ProcessCardManager processCardManager,
+ ProjectDao projectDao,
+ RepositoryDao repositoryDao, ConcordObjectMapper objectMapper) {
+ this.organizationManager = organizationManager;
+ this.processCardManager = processCardManager;
+
+ this.projectDao = projectDao;
+ this.repositoryDao = repositoryDao;
+ this.objectMapper = objectMapper;
+ }
+
+ @GET
+ @Path("/processcard")
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(description = "List user process cards", operationId = "listUserProcessCards")
+ public List list() {
+
+ UserPrincipal user = UserPrincipal.assertCurrent();
+
+ return processCardManager.listUserCards(user.getId());
+ }
+
+ @GET
+ @Path("/processcard/{id}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(description = "Get process card", operationId = "getProcessCard")
+ public ProcessCardEntry get(@PathParam("id") UUID cardId) throws IOException {
+ return assertCard(cardId);
+ }
+
+ @DELETE
+ @Path("/processcard/{id}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(description = "Delete process card", operationId = "deleteProcessCard")
+ public GenericOperationResult delete(@PathParam("id") UUID cardId) throws IOException {
+
+ assertCard(cardId);
+ processCardManager.assertAccess(cardId);
+
+ processCardManager.delete(cardId);
+
+ return new GenericOperationResult(OperationResult.DELETED);
+ }
+
+ @POST
+ @Path("/processcard/{id}/access")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(description = "Process cards access", operationId = "processCardAccess")
+ public GenericOperationResult access(
+ @PathParam("id") UUID cardId,
+ ProcessCardAccessEntry entry) throws IOException {
+
+ assertCard(cardId);
+ processCardManager.assertAccess(cardId);
+
+ processCardManager.updateAccess(cardId, entry.teamIds(), entry.userIds());
+
+ return new GenericOperationResult(OperationResult.UPDATED);
+ }
+
+ @POST
+ @Path("/processcard")
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(description = "Create or update process card", operationId = "createOrUpdateProcessCard")
+ public ProcessCardOperationResponse createOrUpdate(
+ @Parameter(schema = @Schema(type = "object", implementation = ProcessCardRequest.class)) MultipartInput input) throws IOException {
+
+ ProcessCardRequest r = ProcessCardRequest.from(input);
+
+ UUID orgId = organizationManager.assertAccess(r.getOrgId(), r.getOrgName(), false).getId();
+ UUID projectId = assertProject(orgId, r);
+ UUID repoId = getRepo(projectId, r);
+ String name = r.getName();
+ String entryPoint = r.getEntryPoint();
+ String description = r.getDescription();
+ Map data = r.getData();
+ UUID id = r.getId();
+
+ try (InputStream icon = r.getIcon();
+ InputStream form = r.getForm()) {
+ return processCardManager.createOrUpdate(id, projectId, repoId, name, entryPoint, description, icon, form, data);
+ }
+ }
+
+ @GET
+ @Path("/processcard/{cardId}/form")
+ @Produces(MediaType.TEXT_HTML)
+ @WithTimer
+ @Operation(description = "Get process card form", operationId = "getProcessCardForm")
+ @ApiResponse(responseCode = "200", description = "Process form content",
+ content = @Content(mediaType = "text/html", schema = @Schema(type = "string", format = "binary")))
+ public Response getForm(@PathParam("cardId") UUID cardId) {
+
+ assertCard(cardId);
+
+ Optional o = processCardManager.getForm(cardId, src -> {
+ try {
+ java.nio.file.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.isEmpty()) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+
+ return toBinaryResponse(o.get());
+ }
+
+ @GET
+ @Path("/processcard/{cardId}/data.js")
+ @Produces("text/javascript")
+ @WithTimer
+ @Operation(description = "Get process card form data", operationId = "getProcessCardFormData")
+ @ApiResponse(responseCode = "200", description = "Process form data content",
+ content = @Content(mediaType = "text/javascript", schema = @Schema(type = "string", format = "binary")))
+ public Response getFormData(@PathParam("cardId") UUID cardId) {
+ ProcessCardEntry card = assertCard(cardId);
+
+ Map customData = processCardManager.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 ProcessCardEntry assertCard(UUID id) {
+ ProcessCardEntry e = processCardManager.get(id);
+ if (e == null) {
+ throw new ConcordApplicationException("Process card not found: " + id, Response.Status.NOT_FOUND);
+ }
+ return e;
+ }
+
+ private UUID assertProject(UUID orgId, ProcessCardRequest request) {
+ UUID id = request.getProjectId();
+ String name = request.getProjectName();
+ if (id == null && name != null) {
+ if (orgId == null) {
+ throw new ValidationErrorsException("Organization ID or name is required");
+ }
+
+ id = projectDao.getId(orgId, name);
+ if (id == null) {
+ throw new ValidationErrorsException("Project not found: " + name);
+ }
+ }
+ return id;
+ }
+
+ private UUID getRepo(UUID projectId, ProcessCardRequest request) {
+ UUID id = request.getRepoId();
+ String name = request.getRepoName();
+ if (id == null && name != null) {
+ if (projectId == null) {
+ throw new ValidationErrorsException("Project ID or name is required");
+ }
+
+ id = repositoryDao.getId(projectId, name);
+ if (id == null) {
+ throw new ValidationErrorsException("Repository not found: " + name);
+ }
+ }
+ return id;
+ }
+
+ private String formatData(Map data) {
+ return String.format(DATA_FILE_TEMPLATE, objectMapper.toString(data));
+ }
+
+ private static Response toBinaryResponse(java.nio.file.Path file) {
+ return Response.ok((StreamingOutput) out -> {
+ try (InputStream in = Files.newInputStream(file)) {
+ IOUtils.copy(in, out);
+ } finally {
+ Files.delete(file);
+ }
+ }).build();
+ }
+
+}
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResource.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResource.java
deleted file mode 100644
index 7ec5e04899..0000000000
--- a/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResource.java
+++ /dev/null
@@ -1,181 +0,0 @@
-package com.walmartlabs.concord.server.console;
-
-/*-
- * *****
- * Concord
- * -----
- * Copyright (C) 2017 - 2018 Walmart Inc.
- * -----
- * Licensed 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 com.walmartlabs.concord.db.AbstractDao;
-import com.walmartlabs.concord.db.MainDB;
-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.ProcessStatus;
-import com.walmartlabs.concord.server.sdk.metrics.WithTimer;
-import com.walmartlabs.concord.server.sdk.rest.Resource;
-import com.walmartlabs.concord.server.security.UserPrincipal;
-import com.walmartlabs.concord.server.user.UserDao;
-import org.jooq.*;
-
-import javax.inject.Inject;
-import javax.ws.rs.*;
-import javax.ws.rs.core.MediaType;
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.OffsetDateTime;
-import java.time.ZoneId;
-import java.util.*;
-import java.util.stream.Collectors;
-
-import static com.walmartlabs.concord.server.console.UserActivityResponse.ProjectProcesses;
-import static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;
-import static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;
-import static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS;
-import static com.walmartlabs.concord.server.sdk.ProcessStatus.*;
-import static org.jooq.impl.DSL.*;
-
-@Path("/api/service/console/user")
-public class UserActivityResource implements Resource {
-
- private final Set ORG_VISIBLE_STATUSES = new HashSet<>(Collections.singletonList(ProcessStatus.RUNNING));
-
- private final UserDao userDao;
- private final ProcessQueueDao processDao;
- private final ProcessStatsDao processStatsDao;
-
- @Inject
- public UserActivityResource(UserDao userDao, ProcessQueueDao processDao, ProcessStatsDao processStatsDao) {
- this.userDao = userDao;
- this.processDao = processDao;
- this.processStatsDao = processStatsDao;
- }
-
- @GET
- @Path("/activity")
- @Produces(MediaType.APPLICATION_JSON)
- @WithTimer
- public UserActivityResponse activity(@QueryParam("maxProjectsPerOrg") @DefaultValue("5") int maxProjectsPerOrg,
- @QueryParam("maxOwnProcesses") @DefaultValue("5") int maxOwnProcesses) {
-
- UserPrincipal user = UserPrincipal.assertCurrent();
- Set orgIds = userDao.getOrgIds(user.getId());
- OffsetDateTime t = startOfDay();
-
- Map> orgProcesses = processStatsDao.processByOrgs(maxProjectsPerOrg, orgIds, ORG_VISIBLE_STATUSES, t);
-
- Map stats = processStatsDao.getCountByStatuses(orgIds, t, user.getId());
-
- ProcessFilter filter = ProcessFilter.builder()
- .initiator(user.getUsername())
- .orgIds(orgIds)
- .includeWithoutProject(true)
- .limit(maxOwnProcesses)
- .build();
- List lastProcesses = processDao.list(filter);
-
- return new UserActivityResponse(stats, orgProcesses, lastProcesses);
- }
-
- private static OffsetDateTime startOfDay() {
- LocalDateTime startOfDay = LocalDateTime.now().with(LocalTime.MIN);
- return startOfDay.atZone(ZoneId.systemDefault()).toOffsetDateTime();
- }
-
- private static class ProcessStatsDao extends AbstractDao {
-
- @Inject
- protected ProcessStatsDao(@MainDB Configuration cfg) {
- super(cfg);
- }
-
- public Map getCountByStatuses(Set orgIds, OffsetDateTime fromUpdatedAt, UUID initiatorId) {
- DSLContext tx = dsl();
-
- SelectConditionStep> projectIds = select(PROJECTS.PROJECT_ID)
- .from(PROJECTS)
- .where(PROJECTS.ORG_ID.in(orgIds));
-
- SelectConditionStep> q = tx.select(
- when(PROCESS_QUEUE.CURRENT_STATUS.eq(RUNNING.name()), 1).otherwise(0).as(RUNNING.name()),
- when(PROCESS_QUEUE.CURRENT_STATUS.eq(SUSPENDED.name()), 1).otherwise(0).as(SUSPENDED.name()),
- when(PROCESS_QUEUE.CURRENT_STATUS.eq(FINISHED.name()), 1).otherwise(0).as(FINISHED.name()),
- when(PROCESS_QUEUE.CURRENT_STATUS.eq(FAILED.name()), 1).otherwise(0).as(FAILED.name()),
- when(PROCESS_QUEUE.CURRENT_STATUS.eq(ENQUEUED.name()), 1).otherwise(0).as(ENQUEUED.name()))
- .from(PROCESS_QUEUE)
- .where(PROCESS_QUEUE.INITIATOR_ID.eq(initiatorId)
- .and(PROCESS_QUEUE.LAST_UPDATED_AT.greaterOrEqual(fromUpdatedAt))
- .and(or(PROCESS_QUEUE.PROJECT_ID.in(projectIds), PROCESS_QUEUE.PROJECT_ID.isNull())));
-
- Record5 r = tx.select(
- sum(q.field(RUNNING.name(), Integer.class)),
- sum(q.field(SUSPENDED.name(), Integer.class)),
- sum(q.field(FINISHED.name(), Integer.class)),
- sum(q.field(FAILED.name(), Integer.class)),
- sum(q.field(ENQUEUED.name(), Integer.class)))
- .from(q)
- .fetchOne();
-
- Map result = new HashMap<>();
- result.put(RUNNING.name(), r.value1() != null ? r.value1().intValue() : 0);
- result.put(SUSPENDED.name(), r.value2() != null ? r.value2().intValue() : 0);
- result.put(FINISHED.name(), r.value3() != null ? r.value3().intValue() : 0);
- result.put(FAILED.name(), r.value4() != null ? r.value4().intValue() : 0);
- result.put(ENQUEUED.name(), r.value5() != null ? r.value5().intValue() : 0);
- return result;
- }
-
- public Map> processByOrgs(int maxProjectRows,
- Set orgIds,
- Set processStatuses,
- OffsetDateTime fromUpdatedAt) {
-
- DSLContext tx = dsl();
-
- Set statuses = processStatuses.stream().map(Enum::name).collect(Collectors.toSet());
- WindowRowsStep rnField = rowNumber().over().partitionBy(ORGANIZATIONS.ORG_NAME).orderBy(ORGANIZATIONS.ORG_NAME);
-
- SelectHavingStep> a =
- tx.select(ORGANIZATIONS.ORG_NAME, PROJECTS.PROJECT_NAME, count(), rnField)
- .from(PROCESS_QUEUE)
- .innerJoin(PROJECTS)
- .on(PROCESS_QUEUE.PROJECT_ID.eq(PROJECTS.PROJECT_ID)
- .and(PROCESS_QUEUE.CURRENT_STATUS.in(statuses))
- .and(PROCESS_QUEUE.LAST_UPDATED_AT.greaterOrEqual(fromUpdatedAt))
- .and(PROJECTS.ORG_ID.in(orgIds))
- )
- .innerJoin(ORGANIZATIONS)
- .on(ORGANIZATIONS.ORG_ID.eq(PROJECTS.ORG_ID))
- .groupBy(ORGANIZATIONS.ORG_NAME, PROJECTS.PROJECT_NAME);
-
- Result> r = tx.select(a.field(0, String.class), a.field(1, String.class), a.field(2, Integer.class))
- .from(a)
- .where(a.field(rnField).lessOrEqual(maxProjectRows))
- .fetch();
-
- Map> result = new HashMap<>();
- r.forEach(i -> {
- String orgName = i.value1();
- String projectName = i.value2();
- int count = i.value3();
- result.computeIfAbsent(orgName, (k) -> new ArrayList<>()).add(new ProjectProcesses(projectName, count));
- });
- return result;
- }
- }
-}
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
new file mode 100644
index 0000000000..bd48ae9b21
--- /dev/null
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResourceV2.java
@@ -0,0 +1,62 @@
+package com.walmartlabs.concord.server.console;
+
+/*-
+ * *****
+ * Concord
+ * -----
+ * Copyright (C) 2017 - 2018 Walmart Inc.
+ * -----
+ * Licensed 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 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.metrics.WithTimer;
+import com.walmartlabs.concord.server.sdk.rest.Resource;
+import com.walmartlabs.concord.server.security.UserPrincipal;
+
+import javax.inject.Inject;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.util.List;
+
+@Path("/api/v2/service/console/user")
+public class UserActivityResourceV2 implements Resource {
+
+ private final ProcessQueueDao processDao;
+
+ @Inject
+ public UserActivityResourceV2(ProcessQueueDao processDao) {
+ this.processDao = processDao;
+ }
+
+ @GET
+ @Path("/activity")
+ @Produces(MediaType.APPLICATION_JSON)
+ @WithTimer
+ public UserActivityResponse activity(@QueryParam("maxOwnProcesses") @DefaultValue("5") int maxOwnProcesses) {
+
+ UserPrincipal user = UserPrincipal.assertCurrent();
+
+ ProcessFilter filter = ProcessFilter.builder()
+ .initiator(user.getUsername())
+ .includeWithoutProject(true)
+ .limit(maxOwnProcesses)
+ .build();
+ List lastProcesses = processDao.list(filter);
+
+ return new UserActivityResponse(lastProcesses);
+ }
+}
diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResponse.java b/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResponse.java
index 743b607b71..69cf6b7e40 100644
--- a/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResponse.java
+++ b/server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResponse.java
@@ -26,61 +26,24 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.walmartlabs.concord.server.process.ProcessEntry;
+import java.io.Serial;
import java.io.Serializable;
import java.util.List;
-import java.util.Map;
@JsonInclude(Include.NON_EMPTY)
-public class UserActivityResponse implements Serializable {
+public final class UserActivityResponse implements Serializable {
+ @Serial
private static final long serialVersionUID = 1L;
- public static class ProjectProcesses implements Serializable {
-
- private static final long serialVersionUID = 1L;
-
- private final String projectName;
- private final int running;
-
- @JsonCreator
- public ProjectProcesses(@JsonProperty("projectName") String projectName,
- @JsonProperty("running") int running) {
- this.projectName = projectName;
- this.running = running;
- }
-
- public String getProjectName() {
- return projectName;
- }
-
- public int getRunning() {
- return running;
- }
- }
-
- private final Map processStats;
- private final Map> orgProcesses;
private final List processes;
@JsonCreator
- public UserActivityResponse(@JsonProperty("processStats") Map processStats,
- @JsonProperty("orgProcesses") Map> orgProcesses,
- @JsonProperty("processes") List processes) {
-
- this.processStats = processStats;
- this.orgProcesses = orgProcesses;
+ public UserActivityResponse(@JsonProperty("processes") List processes) {
this.processes = processes;
}
- public Map getProcessStats() {
- return processStats;
- }
-
- public Map> getOrgProcesses() {
- return orgProcesses;
- }
-
- public List getProcesses() {
+ public List processes() {
return processes;
}
}