Skip to content

Commit 9243333

Browse files
authored
Merge pull request #224 from SourceLabOrg/sp/configViewDatatable
Update ViewConfig datatable
2 parents 89694ad + 8a97331 commit 9243333

File tree

6 files changed

+175
-91
lines changed

6 files changed

+175
-91
lines changed

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/configuration/view/ViewConfigController.java

Lines changed: 145 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import com.fasterxml.jackson.core.JsonProcessingException;
2828
import com.fasterxml.jackson.databind.ObjectMapper;
29+
import org.apache.kafka.common.errors.TimeoutException;
2930
import org.sourcelab.kafka.webview.ui.controller.BaseController;
3031
import org.sourcelab.kafka.webview.ui.controller.configuration.view.forms.ViewForm;
3132
import org.sourcelab.kafka.webview.ui.manager.kafka.KafkaOperations;
@@ -35,6 +36,11 @@
3536
import org.sourcelab.kafka.webview.ui.manager.model.view.ViewCopyManager;
3637
import org.sourcelab.kafka.webview.ui.manager.ui.BreadCrumbManager;
3738
import org.sourcelab.kafka.webview.ui.manager.ui.FlashMessage;
39+
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.ActionTemplate;
40+
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.Datatable;
41+
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.DatatableColumn;
42+
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.DatatableFilter;
43+
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.LinkTemplate;
3844
import org.sourcelab.kafka.webview.ui.model.Cluster;
3945
import org.sourcelab.kafka.webview.ui.model.Filter;
4046
import org.sourcelab.kafka.webview.ui.model.MessageFormat;
@@ -47,6 +53,7 @@
4753
import org.sourcelab.kafka.webview.ui.repository.ViewRepository;
4854
import org.sourcelab.kafka.webview.ui.repository.ViewToFilterOptionalRepository;
4955
import org.springframework.beans.factory.annotation.Autowired;
56+
import org.springframework.data.domain.Pageable;
5057
import org.springframework.stereotype.Controller;
5158
import org.springframework.transaction.annotation.Transactional;
5259
import org.springframework.ui.Model;
@@ -64,6 +71,7 @@
6471
import java.util.ArrayList;
6572
import java.util.HashMap;
6673
import java.util.HashSet;
74+
import java.util.List;
6775
import java.util.Map;
6876
import java.util.Optional;
6977
import java.util.Set;
@@ -94,17 +102,122 @@ public class ViewConfigController extends BaseController {
94102
@Autowired
95103
private KafkaOperationsFactory kafkaOperationsFactory;
96104

105+
97106
/**
98-
* GET Displays main configuration index.
107+
* GET views index.
99108
*/
100109
@RequestMapping(path = "", method = RequestMethod.GET)
101-
public String index(final Model model) {
110+
public String datatable(
111+
final Model model,
112+
@RequestParam(name = "cluster.id", required = false) final Long clusterId,
113+
final Pageable pageable,
114+
@RequestParam Map<String,String> allParams
115+
) {
102116
// Setup breadcrumbs
103117
setupBreadCrumbs(model, null, null);
104118

105-
// Retrieve all message formats
106-
final Iterable<View> viewList = viewRepository.findAllByOrderByNameAsc();
107-
model.addAttribute("views", viewList);
119+
// Determine if we actually have any clusters setup
120+
// Retrieve all clusters and index by id
121+
final Map<Long, Cluster> clustersById = new HashMap<>();
122+
clusterRepository
123+
.findAllByOrderByNameAsc()
124+
.forEach((cluster) -> clustersById.put(cluster.getId(), cluster));
125+
126+
// Create a filter
127+
final List<DatatableFilter.FilterOption> filterOptions = new ArrayList<>();
128+
clustersById
129+
.forEach((id, cluster) -> filterOptions.add(new DatatableFilter.FilterOption(String.valueOf(id), cluster.getName())));
130+
final DatatableFilter filter = new DatatableFilter("Cluster", "clusterId", filterOptions);
131+
model.addAttribute("filters", new DatatableFilter[] { filter });
132+
133+
final Datatable.Builder<View> builder = Datatable.newBuilder(View.class)
134+
.withRepository(viewRepository)
135+
.withPageable(pageable)
136+
.withRequestParams(allParams)
137+
.withUrl("/configuration/view")
138+
.withLabel("Views")
139+
.withCreateLink("/configuration/view/create")
140+
141+
// Name Column
142+
.withColumn(DatatableColumn.newBuilder(View.class)
143+
.withFieldName("name")
144+
.withLabel("Name")
145+
.withIsSortable(true)
146+
.withRenderTemplate(new LinkTemplate<>(
147+
(record) -> "/view/" + record.getId(),
148+
View::getName
149+
)).build())
150+
151+
// Cluster Column
152+
.withColumn(DatatableColumn.newBuilder(View.class)
153+
.withFieldName("cluster.name")
154+
.withLabel("Cluster")
155+
.withRenderTemplate(new LinkTemplate<>(
156+
(record) -> "/cluster/" + record.getCluster().getId(),
157+
(record) -> record.getCluster().getName()
158+
))
159+
.withIsSortable(true)
160+
.build())
161+
162+
// Topic Column
163+
.withColumn(DatatableColumn.newBuilder(View.class)
164+
.withFieldName("topic")
165+
.withLabel("Topic")
166+
.withRenderFunction(View::getTopic)
167+
.withIsSortable(true)
168+
.build())
169+
170+
// Partitions Column
171+
.withColumn(DatatableColumn.newBuilder(View.class)
172+
.withFieldName("partitions")
173+
.withLabel("Partitions")
174+
.withRenderFunction(View::displayPartitions)
175+
.withIsSortable(false)
176+
.build())
177+
178+
// Key Format Column
179+
.withColumn(DatatableColumn.newBuilder(View.class)
180+
.withFieldName("keyMessageFormat.name")
181+
.withLabel("Key Format")
182+
.withRenderFunction((view) -> view.getKeyMessageFormat().getName())
183+
.withIsSortable(true)
184+
.build())
185+
186+
// Value Format Column
187+
.withColumn(DatatableColumn.newBuilder(View.class)
188+
.withFieldName("valueMessageFormat.name")
189+
.withLabel("Value Format")
190+
.withRenderFunction((view) -> view.getValueMessageFormat().getName())
191+
.withIsSortable(true)
192+
.build())
193+
194+
// Action Column
195+
.withColumn(DatatableColumn.newBuilder(View.class)
196+
.withLabel("Action")
197+
.withFieldName("id")
198+
.withIsSortable(false)
199+
.withHeaderAlignRight()
200+
.withRenderTemplate(ActionTemplate.newBuilder(View.class)
201+
// Edit Link
202+
.withEditLink(View.class, (record) -> "/configuration/view/edit/" + record.getId())
203+
// Copy Link
204+
.withCopyLink(View.class, (record) -> "/configuration/view/copy/" + record.getId())
205+
// Delete Link
206+
.withDeleteLink(View.class, (record) -> "/configuration/view/delete/" + record.getId())
207+
.build())
208+
.build())
209+
210+
// Filters
211+
.withFilter(new DatatableFilter("Cluster", "cluster.id", filterOptions))
212+
.withSearch("name");
213+
214+
// Add datatable attribute
215+
model.addAttribute("datatable", builder.build());
216+
217+
// Determine if we have no clusters setup so we can show appropriate inline help.
218+
model.addAttribute("hasClusters", !clustersById.isEmpty());
219+
model.addAttribute("hasNoClusters", clustersById.isEmpty());
220+
model.addAttribute("hasViews", viewRepository.count() > 0);
108221

109222
return "configuration/view/index";
110223
}
@@ -113,7 +226,7 @@ public String index(final Model model) {
113226
* GET Displays create view form.
114227
*/
115228
@RequestMapping(path = "/create", method = RequestMethod.GET)
116-
public String createViewForm(final ViewForm viewForm, final Model model) {
229+
public String createViewForm(final ViewForm viewForm, final Model model, final RedirectAttributes redirectAttributes) {
117230
// Setup breadcrumbs
118231
if (!model.containsAttribute("BreadCrumbs")) {
119232
setupBreadCrumbs(model, "Create", null);
@@ -147,10 +260,28 @@ public String createViewForm(final ViewForm viewForm, final Model model) {
147260
final TopicDetails topicDetails = operations.getTopicDetails(viewForm.getTopic());
148261
model.addAttribute("partitions", topicDetails.getPartitions());
149262
}
263+
redirectAttributes.getFlashAttributes().remove("ClusterError");
264+
} catch (final TimeoutException timeoutException) {
265+
final String reason = timeoutException.getMessage() + " (This may indicate an authentication or connection problem)";
266+
// Set error msg
267+
redirectAttributes.addFlashAttribute("ClusterError", true);
268+
redirectAttributes.addFlashAttribute(
269+
"FlashMessage",
270+
FlashMessage.newDanger(
271+
"Error connecting to cluster '" + cluster.getName() + "': " + reason,
272+
timeoutException
273+
)
274+
);
150275
}
151276
});
152277
}
153278

279+
// If we had an error.
280+
if (redirectAttributes.getFlashAttributes().containsKey("ClusterError")) {
281+
// Redirect instead.
282+
return "redirect:/configuration/view";
283+
}
284+
154285
return "configuration/view/create";
155286
}
156287

@@ -215,7 +346,7 @@ public String editViewForm(
215346
}
216347
viewForm.setOptionalFilters(optionalFilterIds);
217348

218-
return createViewForm(viewForm, model);
349+
return createViewForm(viewForm, model, redirectAttributes);
219350
}
220351

221352
/**
@@ -246,7 +377,7 @@ public String updateView(
246377

247378
// If we have errors
248379
if (bindingResult.hasErrors()) {
249-
return createViewForm(viewForm, model);
380+
return createViewForm(viewForm, model, redirectAttributes);
250381
}
251382

252383
// If we're updating
@@ -484,9 +615,13 @@ public String copyView(@PathVariable final Long id, final RedirectAttributes red
484615
viewRepository.findById(id).ifPresent((view) -> {
485616
// Create Copy manager
486617
final ViewCopyManager copyManager = new ViewCopyManager(viewRepository);
487-
copyManager.copy(view, "Copy of " + view.getName());
618+
final String newName = "Copy of " + view.getName();
619+
copyManager.copy(view, newName);
488620

489-
redirectAttributes.addFlashAttribute("FlashMessage", FlashMessage.newSuccess("Copied view!"));
621+
redirectAttributes.addFlashAttribute(
622+
"FlashMessage",
623+
FlashMessage.newSuccess("Copied view using name '" + newName + "'!")
624+
);
490625
});
491626
}
492627

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/view/ViewController.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,18 @@ public String datatable(
131131
.withLabel("View")
132132
.withRenderFunction((View::getName))
133133
.build())
134-
.withColumn(DatatableColumn.newBuilder(View.class)
135-
.withFieldName("topic")
136-
.withLabel("Topic")
137-
.withRenderFunction(View::getTopic)
138-
.build())
139134
.withColumn(DatatableColumn.newBuilder(View.class)
140135
.withFieldName("cluster.name")
141136
.withLabel("Cluster")
142137
.withRenderTemplate(new LinkTemplate<>(
143138
(record) -> "/cluster/" + record.getCluster().getId(),
144139
(record) -> record.getCluster().getName()
145140
)).build())
141+
.withColumn(DatatableColumn.newBuilder(View.class)
142+
.withFieldName("topic")
143+
.withLabel("Topic")
144+
.withRenderFunction(View::getTopic)
145+
.build())
146146
.withColumn(DatatableColumn.newBuilder(View.class)
147147
.withLabel("")
148148
.withFieldName("")

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/datatable/ActionTemplate.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,26 @@ public Builder<T> withDeleteLink(
195195
);
196196
}
197197

198+
/**
199+
* Add a standard Copy link.
200+
* @param type Record type.
201+
* @param urlFunction The url to link to.
202+
* @return Builder instance.
203+
*/
204+
public Builder<T> withCopyLink(
205+
final Class<T> type,
206+
final Function<T, String> urlFunction
207+
) {
208+
return withLink(
209+
ActionTemplate.ActionLink.newBuilder(type)
210+
.withLabelFunction((record) -> "Copy")
211+
.withUrlFunction(urlFunction)
212+
.withIcon("fa-copy")
213+
.withIsPost(true)
214+
.build()
215+
);
216+
}
217+
198218
/**
199219
* Create new ActionTemplate instance from Builder.
200220
* @return ActionType instance.

kafka-webview-ui/src/main/resources/templates/configuration/messageFormat/create.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ <h6>
177177
<script id="options-template" type="text/x-handlebars-template">
178178
<!-- User Defined Option -->
179179
<div class="form-group row custom-option">
180-
<label class="col-md-3 form-control-label" for="classpath">
180+
<label class="col-md-3 form-control-label">
181181
<i>{{optionName}}</i>
182182
</label>
183183
<div class="col-md-8">

kafka-webview-ui/src/main/resources/templates/configuration/view/index.html

Lines changed: 2 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -11,80 +11,8 @@
1111
<body>
1212
<section layout:fragment="content">
1313
<div class="container">
14-
<div class="row">
15-
<div class="col-lg-12">
16-
<div class="card">
17-
<div class="card-header">
18-
<i class="fa fa-align-justify"></i>
19-
Views
20-
<div class="btn-group float-right" role="group" aria-label="Button group">
21-
<a class="btn" th:href="@{/configuration/view/create}" style="padding-bottom: 0;">
22-
<i class="icon-settings"></i>
23-
&nbsp;Create new
24-
</a>
25-
</div>
26-
</div>
27-
<div class="card-body">
28-
<table class="table table-bordered table-striped table-sm">
29-
<thead>
30-
<tr>
31-
<th>Name</th>
32-
<th>Cluster</th>
33-
<th>Topic</th>
34-
<th>Partitions</th>
35-
<th>Key Format</th>
36-
<th>Value Format</th>
37-
<th class="text-right">Action</th>
38-
</tr>
39-
</thead>
40-
<tbody>
41-
<tr th:if="${views.isEmpty()}" align="center">
42-
<td colspan="7">
43-
No views found!
44-
</td>
45-
</tr>
46-
<tr th:each="view : ${views}">
47-
<td>
48-
<a th:href="@{/view/{id}(id=${view.id})}" th:text="${view.name}"/>
49-
</td>
50-
<td th:text="${view.cluster.name}"></td>
51-
<td th:text="${view.topic}"></td>
52-
<td th:text="${view.displayPartitions()}"></td>
53-
<td th:text="${view.keyMessageFormat.name}"></td>
54-
<td th:text="${view.valueMessageFormat.name}"></td>
55-
<td class="text-right">
56-
<div class="dropdown">
57-
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
58-
Actions
59-
</button>
60-
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
61-
<a class="dropdown-item" th:href="@{/configuration/view/edit/{id}(id=${view.id})}">
62-
<i class="fa fa-edit"></i>
63-
Edit
64-
</a>
65-
<form th:action="@{/configuration/view/copy/{id}(id=${view.id})}" method="post">
66-
<button class="dropdown-item" type="submit" style="border-bottom: 1px solid #c2cfd6;">
67-
<i class="fa fa-copy"></i>
68-
Copy
69-
</button>
70-
</form>
71-
<form th:action="@{/configuration/view/delete/{id}(id=${view.id})}" method="post">
72-
<button class="dropdown-item" onclick="return confirm('Are you sure?');" type="submit">
73-
<i class="fa fa-remove"></i>
74-
Delete
75-
</button>
76-
</form>
77-
</div>
78-
</div>
79-
</td>
80-
</tr>
81-
</tbody>
82-
</table>
83-
</div>
84-
</div>
85-
</div>
86-
<!--/.col-->
87-
</div>
14+
<!-- Render datatable -->
15+
<div th:replace="fragments/datatable/Datatable :: display(${datatable})"></div>
8816
</div>
8917
</section>
9018

kafka-webview-ui/src/main/resources/templates/fragments/datatable/fields/Action.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
<form
2525
th:if="${action.isPost()}"
2626
th:action="@{${action.getUrl(record)}}"
27-
method="post">
27+
method="post"
28+
style="border-bottom: 1px solid #c2cfd6;">
2829
<button class="dropdown-item" onclick="return confirm('Are you sure?');" type="submit">
2930
<i th:if="${action.hasIcon()}"
3031
th:class="'fa ' + ${action.getIcon()}"></i>

0 commit comments

Comments
 (0)