Skip to content

Commit e79b0d7

Browse files
committed
Updated UI for workflows
Split into css/html/js files and addressed review comments Fixed bidirectional edge issue, and added a way to delete edge Added dag class in UI to detect any cyclic dependency Fixed error of not resetting dag when edge or launcher gets deleted at the UI Added grid logic, each launcher will have fixed spot and no one can take anyone else's place. Plus scrolling logic improved A little formatting of JS files Added zoom functionality Fixed size st. launcher should fit and added mechanism to allow increase of grid till 32x32~1024 launchers Added box, edge, pointer class and refactored code a little; next will add dragController class Added dragController class, pending CI issue Made launcher identical to those on project page, should fix tests Fix the tests by allowing grabbing by title only Changed launcher-item to launcher-box to isolate ourselves from scss of projects Changed top/left style to transfrom for smooth transitions Fixed double click on launcher, added accent to buttons
1 parent 21f13f3 commit e79b0d7

File tree

8 files changed

+850
-62
lines changed

8 files changed

+850
-62
lines changed

apps/dashboard/app/assets/stylesheets/application.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ small.form-text {
178178
@import "support_ticket";
179179
@import "data_tables";
180180
@import "projects";
181+
@import "workflows";
181182
@import "scripts";
182183
@import "common";
183184
@import "module_browser";
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
$cols: 16;
2+
$rows: 16;
3+
// Now we can increse grid size in 1:1 ratio to keep workspace wrapper size same
4+
$cell_w: 204; // (204+20) * 4 = 900px width for workspace wrapper
5+
$cell_h: 130; // (130+20) * 4 = 600px height for workspace wrapper
6+
$gap: 20;
7+
8+
:root {
9+
--bg: #f7f7fb;
10+
--ink: #222;
11+
--ink-muted: #666;
12+
--accent: #2266ff;
13+
--accent-weak: #cfe0ff;
14+
--danger: #b71c1c;
15+
--danger-weak: #c628285c;
16+
--box-selected: #ffecb3;
17+
// To pass on the variable to javascript
18+
--grid_cols: #{$cols};
19+
--grid_rows: #{$rows};
20+
--cell_w: #{$cell_w};
21+
--cell_h: #{$cell_h};
22+
--gap: #{$gap};
23+
}
24+
25+
html, body {
26+
height: 100%;
27+
margin: 0;
28+
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
29+
color: var(--ink);
30+
}
31+
32+
.app {
33+
display: grid;
34+
grid-template-rows: auto 1fr;
35+
height: 100%;
36+
background: var(--bg);
37+
}
38+
39+
.toolbar {
40+
display: flex;
41+
gap: .5rem;
42+
align-items: center;
43+
padding: .5rem .75rem;
44+
border-bottom: 1px solid #e6e8ee;
45+
background: #fff;
46+
position: sticky;
47+
top: 0;
48+
z-index: 5;
49+
}
50+
51+
.toolbar button {
52+
border: 1.5px solid #d5d8e0;
53+
background: #fff;
54+
border-radius: 10px;
55+
padding: .35rem .7rem;
56+
cursor: pointer;
57+
font-weight: 600;
58+
transition: all 0.15s ease-in-out;
59+
}
60+
61+
.toolbar button.active {
62+
border-color: var(--accent);
63+
box-shadow: 0 0 0 4px var(--accent-weak);
64+
transform: scale(0.98);
65+
}
66+
67+
.toolbar .danger:active {
68+
border-color: var(--danger);
69+
box-shadow: 0 0 0 4px var(--danger-weak);
70+
transform: scale(0.98);
71+
}
72+
73+
.hint {
74+
margin-left: auto;
75+
color: var(--ink-muted);
76+
font-size: .9rem;
77+
}
78+
79+
.workspace-wrapper {
80+
position: relative;
81+
width: 100%;
82+
height: 600px;
83+
overflow: hidden;
84+
border: 1px solid #ddd;
85+
}
86+
87+
.zoom-controls {
88+
position: absolute;
89+
left: 2px;
90+
top: 50%;
91+
transform: translateY(-50%);
92+
display: flex;
93+
flex-direction: column;
94+
gap: 0.5rem;
95+
z-index: 20;
96+
user-select: none;
97+
}
98+
99+
.zoom-controls button {
100+
width: 45px;
101+
height: 30px;
102+
border-radius: 8px;
103+
border: 1px solid #d5d8e0;
104+
background: #fff;
105+
font-weight: 700;
106+
cursor: pointer;
107+
padding: 0;
108+
display: inline-flex;
109+
align-items: center;
110+
justify-content: center;
111+
}
112+
113+
.zoom-controls button:active {
114+
transform: translateY(1px);
115+
}
116+
117+
.zoom-controls button[aria-pressed="true"] {
118+
box-shadow: 0 0 0 3px var(--accent-weak);
119+
border-color: var(--accent);
120+
}
121+
122+
.workspace {
123+
position: absolute;
124+
top: 0;
125+
left: 0;
126+
right: 0;
127+
bottom: 0;
128+
overflow: auto;
129+
background: var(--bg);
130+
}
131+
132+
.stage-zoom {
133+
width: 100%;
134+
height: 100%;
135+
transform-origin: top left;
136+
will-change: transform;
137+
}
138+
139+
.stage {
140+
display: grid;
141+
grid-template-columns: repeat(#{$cols}, #{$cell_w});
142+
grid-template-rows: repeat(#{$rows}, #{$cell_h});
143+
gap: #{$gap};
144+
background: repeating-conic-gradient(#fafbff 0% 25%, #f5f7ff 0% 50%) 50%/20px 20px;
145+
padding: 20px;
146+
// count * width + (count-1) * gap
147+
min-width: calc(#{$cols} * #{$cell_w}px + (#{$cols} - 1) * #{$gap}px);
148+
min-height: calc(#{$rows} * #{$cell_h}px + (#{$rows} - 1) * #{$gap}px);
149+
position: relative;
150+
}
151+
152+
svg.edges {
153+
position: absolute;
154+
inset: 0;
155+
pointer-events: none;
156+
z-index: 1;
157+
}
158+
159+
.edge {
160+
stroke: var(--accent);
161+
stroke-width: 2.5;
162+
marker-end: url(#arrow);
163+
cursor: pointer;
164+
pointer-events: all;
165+
}
166+
167+
.edge.selected {
168+
stroke: var(--danger);
169+
stroke-width: 4;
170+
filter: drop-shadow(0 0 4px var(--danger-weak));
171+
}
172+
173+
.launcher-box {
174+
position: absolute;
175+
background: #fff;
176+
border-radius: 10px;
177+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
178+
user-select: none;
179+
padding: 0.5rem;
180+
transform-origin: top left;
181+
transition: transform 0.15s;
182+
will-change: transform;
183+
}
184+
185+
.launcher-title-grab {
186+
font-size: 1em;
187+
font-weight: bold;
188+
cursor: grab;
189+
user-select: none;
190+
}
191+
192+
.launcher-box.selected {
193+
outline: 3px solid var(--accent-weak);
194+
background: var(--box-selected);
195+
}
196+
197+
.launcher-box.connect-queued {
198+
outline: 3px dashed var(--accent);
199+
}

apps/dashboard/app/controllers/launchers_controller.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ def submit
7575
end
7676
end
7777

78+
# GET /projects/:project_id/launchers/:id/render_button
79+
def render_button
80+
launcher = Launcher.find(show_launcher_params[:id], @project.directory)
81+
@valid_project = Launcher.clusters?
82+
@remove_delete_button = true
83+
render(partial: 'projects/launcher_buttons', locals: { launcher: launcher })
84+
end
85+
7886
private
7987

8088
def find_launcher
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// This class will help us to detect if any new edge can lead to cycle or not
2+
// Thus we can alert user to avoid that edge thus resolving cycle issue on UI
3+
4+
// Directed Acyclic Graph
5+
export class DAG {
6+
#launcher_list;
7+
#adjacency_list;
8+
#visited;
9+
#stack;
10+
#has_cycle;
11+
12+
constructor() {
13+
this.#launcher_list = new Set();
14+
this.#adjacency_list = {};
15+
}
16+
17+
addEdge(fromId, toId) {
18+
if (!this.#launcher_list.has(fromId)) {
19+
this.#launcher_list.add(fromId);
20+
}
21+
22+
if (!this.#launcher_list.has(toId)) {
23+
this.#launcher_list.add(toId);
24+
}
25+
26+
if (!this.#adjacency_list[fromId]) {
27+
this.#adjacency_list[fromId] = [];
28+
}
29+
this.#adjacency_list[fromId].push(toId);
30+
31+
this.#has_cycle = false;
32+
this.#visited = new Set();
33+
this.#stack = new Set();
34+
this.#launcher_list.forEach(l => this.#detectCycle(l));
35+
36+
// Remove the added edge from the adjacency list if cycle detected
37+
if (this.#has_cycle === true) {
38+
this.#adjacency_list[fromId].pop();
39+
}
40+
}
41+
42+
removeEdge(fromId, toId) {
43+
if (!this.#adjacency_list[fromId]) return;
44+
45+
if (this.#adjacency_list[fromId].includes(toId)) {
46+
this.#adjacency_list[fromId] = this.#adjacency_list[fromId].filter(x => x !== toId);
47+
}
48+
}
49+
50+
removeLauncher(id) {
51+
this.#launcher_list.delete(id);
52+
delete this.#adjacency_list[id];
53+
54+
for (const from in this.#adjacency_list) {
55+
this.#adjacency_list[from] = this.#adjacency_list[from].filter(x => x !== id);
56+
}
57+
}
58+
59+
hasCycle() {
60+
return this.#has_cycle;
61+
}
62+
63+
// Basic dfs on graph to find a cycle
64+
#detectCycle(id) {
65+
if (this.#stack.has(id)) {
66+
this.#has_cycle = true;
67+
return;
68+
}
69+
if (this.#visited.has(id)) return;
70+
71+
this.#stack.add(id);
72+
this.#visited.add(id);
73+
for (const l of this.#adjacency_list[id] || []) {
74+
this.#detectCycle(l);
75+
}
76+
this.#stack.delete(id);
77+
}
78+
}

0 commit comments

Comments
 (0)