Skip to content

Commit 58185df

Browse files
committed
Smart integration: integrate statistics view and regeneration into UI
- Implement a panel for displaying and regenerating object class statistics - Integrate the panel into object class tile view - Integrate the panel into resource object view - Extract generic progress handling into SmartTaskProgressPanel - Disable cross-table (tuple) view in object class statistics
1 parent 08efa55 commit 58185df

File tree

13 files changed

+366
-63
lines changed

13 files changed

+366
-63
lines changed

gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/resource/component/ResourceUncategorizedPanel.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<h3 wicket:id="title" class="m-0"/>
1212
<div wicket:id="objectType"/>
1313
<div wicket:id="tasks"/>
14+
<div wicket:id="statistics"/>
1415
</div>
1516
<div wicket:id="table"/>
1617
</wicket:panel>

gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/resource/component/ResourceUncategorizedPanel.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.util.function.Consumer;
1414
import javax.xml.namespace.QName;
1515

16+
import com.evolveum.midpoint.gui.impl.page.admin.resource.component.wizard.schemaHandling.objectType.smart.table.ObjectClassStatisticsButton;
17+
1618
import org.apache.commons.lang3.StringUtils;
1719
import org.apache.wicket.Component;
1820
import org.apache.wicket.ajax.AjaxRequestTarget;
@@ -66,6 +68,7 @@ public class ResourceUncategorizedPanel extends AbstractResourceObjectPanel {
6668
private static final String ID_TABLE = "table";
6769
private static final String ID_TITLE = "title";
6870
private static final String ID_TASKS = "tasks";
71+
private static final String ID_STATISTICS = "statistics";
6972

7073
public ResourceUncategorizedPanel(String id, ResourceDetailsModel model, ContainerPanelConfigurationType config) {
7174
super(id, model, config);
@@ -137,9 +140,20 @@ protected void initLayout() {
137140
createPanelTitle();
138141
createObjectTypeChoice();
139142
createTasksButton(ID_TASKS);
143+
createStatisticsButton();
140144
createShadowTable();
141145
}
142146

147+
private void createStatisticsButton() {
148+
ResourceDetailsModel objectDetailsModels = getObjectDetailsModels();
149+
ResourceType resource = objectDetailsModels.getObjectType();
150+
151+
ObjectClassStatisticsButton statisticsButton = new ObjectClassStatisticsButton(ID_STATISTICS,
152+
this::getObjectClass, resource.getOid());
153+
statisticsButton.setOutputMarkupId(true);
154+
add(statisticsButton);
155+
}
156+
143157
private void createPanelTitle() {
144158
Label title = new Label(ID_TITLE, createStringResource("ResourceUncategorizedPanel.select.objectClass.title"));
145159
title.add(getTitleVisibleBehaviour());

gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/resource/component/wizard/schemaHandling/objectType/smart/component/SmartStatisticsPanel.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
<!--
2-
~ Copyright (c) 2010-2025 Evolveum
2+
~ Copyright (C) 2010-2026 Evolveum and contributors
33
~
4-
~ This work is dual-licensed under the Apache License 2.0
5-
~ and European Union Public License. See LICENSE file for details.
4+
~ Licensed under the EUPL-1.2 or later.
65
-->
76
<!DOCTYPE html>
87
<html xmlns:wicket="http://wicket.apache.org">
@@ -65,9 +64,10 @@
6564
</wicket:fragment>
6665

6766
<wicket:fragment wicket:id="title">
68-
<div class="d-flex justify-content-between align-items-center w-100">
67+
<div class="d-flex align-items-center gap-2 w-100">
6968
<div class="h5 m-0" wicket:id="primaryTitle"></div>
70-
<div class="text-muted small" wicket:id="secondaryTitle"></div>
69+
<div class="text-muted small ml-auto" wicket:id="secondaryTitle"></div>
70+
<div wicket:id="regenerateButton"></div>
7171
</div>
7272
</wicket:fragment>
7373
</wicket:panel>

gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/resource/component/wizard/schemaHandling/objectType/smart/component/SmartStatisticsPanel.java

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/*
2-
* Copyright (C) 2010-2025 Evolveum and contributors
2+
* Copyright (C) 2010-2026 Evolveum and contributors
33
*
4-
* This work is dual-licensed under the Apache License 2.0
5-
* and European Union Public License. See LICENSE file for details.\
4+
* Licensed under the EUPL-1.2 or later.
65
*/
76
package com.evolveum.midpoint.gui.impl.page.admin.resource.component.wizard.schemaHandling.objectType.smart.component;
87

@@ -13,6 +12,7 @@
1312
import com.evolveum.midpoint.gui.api.component.TogglePanel;
1413
import com.evolveum.midpoint.gui.api.model.LoadableModel;
1514
import com.evolveum.midpoint.gui.impl.component.data.provider.ListDataProvider;
15+
import com.evolveum.midpoint.gui.impl.page.admin.resource.component.wizard.schemaHandling.objectType.smart.table.ObjectClassStatisticsButton;
1616
import com.evolveum.midpoint.gui.impl.page.admin.role.mining.page.panel.outlier.MetricValuePanel;
1717
import com.evolveum.midpoint.gui.impl.page.admin.role.mining.page.tmp.panel.IconWithLabel;
1818
import com.evolveum.midpoint.web.component.data.BoxedTablePanel;
@@ -60,6 +60,17 @@
6060
import static com.evolveum.midpoint.gui.impl.page.admin.role.mining.RoleAnalysisWebUtils.CLASS_CSS;
6161
import static com.evolveum.midpoint.gui.impl.page.admin.role.mining.RoleAnalysisWebUtils.STYLE_CSS;
6262

63+
/**
64+
* Popup panel that displays computed statistics for a resource object class.
65+
*
66+
* <p>The panel visualizes object class statistics such as attribute values,
67+
* value patterns, frequencies, and basic summary metrics. Users can browse
68+
* individual attributes, inspect their statistics, and trigger regeneration
69+
* of statistics if needed.</p>
70+
*
71+
* <p>The panel is shown in a modal dialog and serves purely as a presentation
72+
* layer for already computed statistics.</p>
73+
*/
6374
//TODO (this is initial implementation)
6475
public class SmartStatisticsPanel extends BasePanel<ShadowObjectClassStatisticsType> implements Popupable {
6576

@@ -89,6 +100,7 @@ public class SmartStatisticsPanel extends BasePanel<ShadowObjectClassStatisticsT
89100
private static final String ID_HEADER_FRAGMENT = "title";
90101
private static final String ID_HEADER_PRIMARY_TITLE = "primaryTitle";
91102
private static final String ID_HEADER_SECONDARY_TITLE = "secondaryTitle";
103+
private static final String ID_HEADER_REGENERATE_BUTTON = "regenerateButton";
92104

93105
private final String resourceOid;
94106
private final QName objectClassName;
@@ -196,7 +208,7 @@ public record ListViewRow(IModel<String> text, IModel<String> subText, Object it
196208

197209
private @NotNull TogglePanel<String> buildToggle() {
198210
IModel<List<Toggle<String>>> toggleModel = () -> List.of(
199-
createToggle("Tuples", isAttributeTuple),
211+
// createToggle("Tuples", isAttributeTuple), // cross table disabled
200212
createToggle("Attributes", !isAttributeTuple)
201213
);
202214

@@ -323,7 +335,6 @@ protected void onEvent(AjaxRequestTarget target) {
323335
main.addOrReplace(buildFrequencyTable(selectedAttribute.getObject(), total));
324336
main.addOrReplace(buildTupleTable(selectedTuple.getObject()));
325337

326-
327338
return main;
328339
}
329340

@@ -429,6 +440,17 @@ public void populateItem(Item<ICellPopulator<R>> cell, String cid, IModel<R> row
429440
protected boolean hideFooterIfSinglePage() {
430441
return true;
431442
}
443+
444+
@Override
445+
public boolean displayIsolatedNoValuePanel() {
446+
return provider.size() == 0;
447+
}
448+
449+
450+
@Override
451+
protected StringResourceModel getNoValuePanelCustomSubTitleModel() {
452+
return createStringResource("SmartStatisticsPanel.noValuePanel.customSubTitle");
453+
}
432454
};
433455
table.setItemsPerPage(5);
434456
frag.add(table);
@@ -559,6 +581,22 @@ public IModel<String> getTitleIconClass() {
559581
Fragment header = new Fragment(SmartStatisticsPanel.ID_HEADER_FRAGMENT, ID_HEADER_FRAGMENT, this);
560582
header.add(new Label(ID_HEADER_PRIMARY_TITLE, getTitle()));
561583
header.add(new Label(ID_HEADER_SECONDARY_TITLE, secondaryTitle).setVisible(secondaryTitle != null));
584+
ObjectClassStatisticsButton statisticsButton = new ObjectClassStatisticsButton(ID_HEADER_REGENERATE_BUTTON,
585+
() -> objectClassName, resourceOid) {
586+
@Override
587+
protected boolean forceRegeneration() {
588+
return true;
589+
}
590+
591+
@Override
592+
protected IModel<String> getMainButtonLabel() {
593+
return createStringResource("SmartStatisticsPanel.regenerateStatistics");
594+
}
595+
};
596+
header.add(statisticsButton);
597+
598+
statisticsButton.setOutputMarkupId(true);
599+
562600
header.add(AttributeAppender.append(CLASS_CSS, "flex-grow-1 mt-1"));
563601
return header;
564602
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!--
2+
~ Copyright (C) 2010-2026 Evolveum and contributors
3+
~
4+
~ Licensed under the EUPL-1.2 or later.
5+
-->
6+
<!DOCTYPE html>
7+
<html xmlns:wicket="http://wicket.apache.org">
8+
<wicket:panel>
9+
10+
<div class="btn btn-sm btn-primary" wicket:id="processStatisticsButton"></div>
11+
12+
</wicket:panel>
13+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright (C) 2010-2026 Evolveum and contributors
3+
*
4+
* Licensed under the EUPL-1.2 or later.
5+
*/
6+
7+
package com.evolveum.midpoint.gui.impl.page.admin.resource.component.wizard.schemaHandling.objectType.smart.table;
8+
9+
import java.io.Serial;
10+
11+
import javax.xml.namespace.QName;
12+
13+
import com.evolveum.midpoint.gui.impl.page.admin.task.component.SmartTaskProgressPanel;
14+
15+
import org.apache.wicket.ajax.AjaxRequestTarget;
16+
import org.apache.wicket.model.IModel;
17+
import org.apache.wicket.model.Model;
18+
import org.jetbrains.annotations.NotNull;
19+
import org.jetbrains.annotations.Nullable;
20+
21+
import com.evolveum.midpoint.gui.api.component.BasePanel;
22+
import com.evolveum.midpoint.gui.api.page.PageBase;
23+
import com.evolveum.midpoint.gui.api.util.WebModelServiceUtils;
24+
import com.evolveum.midpoint.gui.impl.page.admin.resource.component.wizard.schemaHandling.objectType.smart.component.SmartStatisticsPanel;
25+
import com.evolveum.midpoint.prism.PrismObject;
26+
import com.evolveum.midpoint.schema.util.ShadowObjectClassStatisticsTypeUtil;
27+
import com.evolveum.midpoint.smart.api.SmartIntegrationService;
28+
import com.evolveum.midpoint.task.api.Task;
29+
import com.evolveum.midpoint.util.exception.CommonException;
30+
import com.evolveum.midpoint.util.exception.SchemaException;
31+
import com.evolveum.midpoint.web.component.AjaxIconButton;
32+
import com.evolveum.midpoint.xml.ns._public.common.common_3.GenericObjectType;
33+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowObjectClassStatisticsType;
34+
import com.evolveum.midpoint.xml.ns._public.common.common_3.TaskType;
35+
36+
/**
37+
* UI button panel for displaying or regenerating object class statistics.
38+
*
39+
* <p>When clicked, the button tries to load the latest statistics for the given
40+
* resource object class. If statistics exist, they are displayed in a popup.
41+
* If they do not exist (or regeneration is forced), a background task is started
42+
* to compute new statistics and a progress popup is shown.</p>
43+
*
44+
* <p>After the computation finishes, the newly generated statistics are
45+
* automatically displayed.</p>
46+
*/
47+
public class ObjectClassStatisticsButton extends BasePanel<QName> {
48+
49+
@Serial private static final long serialVersionUID = 1L;
50+
51+
private static final String ID_PROCESS_STATISTICS_BUTTON = "processStatisticsButton";
52+
53+
private static final String OPERATION_GET_STATISTICS = "getObjectClassStatistics";
54+
private static final String OPERATION_REGENERATE_STATISTICS = "regenerateObjectClassStatistics";
55+
private static final String OP_LOAD_TASK = "loadTask";
56+
57+
private final String resourceOid;
58+
59+
public ObjectClassStatisticsButton(String id, IModel<QName> objectClassName, String resourceOid) {
60+
super(id, objectClassName);
61+
this.resourceOid = resourceOid;
62+
}
63+
64+
@Override
65+
protected void onInitialize() {
66+
super.onInitialize();
67+
initLayout();
68+
}
69+
70+
private void initLayout() {
71+
AjaxIconButton processStatisticsButton = new AjaxIconButton(
72+
ID_PROCESS_STATISTICS_BUTTON,
73+
Model.of("fa-solid fa-chart-bar"),
74+
getMainButtonLabel()) {
75+
76+
@Serial private static final long serialVersionUID = 1L;
77+
78+
@Override
79+
public void onClick(AjaxRequestTarget target) {
80+
handleClick(target);
81+
}
82+
};
83+
processStatisticsButton.showTitleAsLabel(true);
84+
add(processStatisticsButton);
85+
}
86+
87+
protected IModel<String> getMainButtonLabel() {
88+
return createStringResource("ObjectClassStatisticsButton.processStatistics");
89+
}
90+
91+
private void handleClick(@NotNull AjaxRequestTarget target) {
92+
PageBase page = getPageBase();
93+
QName objectClassName = getModelObject();
94+
SmartIntegrationService smartIntegrationService = page.getSmartIntegrationService();
95+
96+
Task task = page.createSimpleTask(OPERATION_GET_STATISTICS);
97+
98+
try {
99+
if (!forceRegeneration()) {
100+
GenericObjectType latestStatistics =
101+
smartIntegrationService.getLatestStatistics(resourceOid, objectClassName, task.getResult());
102+
103+
if (latestStatistics != null) {
104+
showStatisticsPopup(target, page, latestStatistics, objectClassName);
105+
return;
106+
}
107+
}
108+
109+
// No stats exist -> trigger regeneration and show progress popup
110+
String taskOid = smartIntegrationService.regenerateObjectClassStatistics(
111+
resourceOid, objectClassName, task, task.getResult());
112+
113+
showProgressPopup(target, getPageBase(), smartIntegrationService, objectClassName, taskOid);
114+
115+
} catch (CommonException e) {
116+
page.error("Couldn't get object class statistics for " + objectClassName + ": " + e.getMessage());
117+
target.add(page.getFeedbackPanel());
118+
} finally {
119+
task.getResult().computeStatusIfUnknown();
120+
}
121+
}
122+
123+
protected boolean forceRegeneration() {
124+
return false;
125+
}
126+
127+
private void showStatisticsPopup(
128+
@NotNull AjaxRequestTarget target,
129+
@NotNull PageBase pageBase, @NotNull GenericObjectType statisticsObject,
130+
@NotNull QName objectClassName) throws SchemaException {
131+
132+
ShadowObjectClassStatisticsType statistics =
133+
ShadowObjectClassStatisticsTypeUtil.getStatisticsRequired(statisticsObject);
134+
135+
SmartStatisticsPanel statisticsPanel = new SmartStatisticsPanel(
136+
pageBase.getMainPopupBodyId(),
137+
() -> statistics,
138+
resourceOid,
139+
objectClassName);
140+
141+
pageBase.replaceMainPopup(statisticsPanel, target);
142+
}
143+
144+
private void showProgressPopup(
145+
@NotNull AjaxRequestTarget target,
146+
@NotNull PageBase pageBase,
147+
@NotNull SmartIntegrationService smartIntegrationService,
148+
@NotNull QName objectClassName,
149+
@NotNull String taskOid) {
150+
151+
SmartTaskProgressPanel panel = new SmartTaskProgressPanel(
152+
pageBase.getMainPopupBodyId(),
153+
createStringResource("ObjectClassStatisticsButton.regeneratingStatistics"),
154+
createStringResource("ObjectClassStatisticsButton.regeneratingStatistics.subText", objectClassName.getLocalPart()),
155+
() -> loadTask(pageBase, taskOid)) {
156+
157+
@Override
158+
protected void onShowResults(AjaxRequestTarget target) {
159+
onProgressFinished(target, pageBase, smartIntegrationService, objectClassName);
160+
}
161+
162+
@Override
163+
protected IModel<String> getStopButtonLabel() {
164+
return createStringResource("ObjectClassStatisticsButton.stopRegeneration");
165+
}
166+
};
167+
168+
pageBase.replaceMainPopup(panel, target);
169+
}
170+
171+
private void onProgressFinished(
172+
@NotNull AjaxRequestTarget target,
173+
@NotNull PageBase pageBase,
174+
@NotNull SmartIntegrationService smartIntegrationService,
175+
@NotNull QName objectClassName) {
176+
177+
Task task = pageBase.createSimpleTask(OPERATION_REGENERATE_STATISTICS);
178+
179+
try {
180+
GenericObjectType latestStatistics =
181+
smartIntegrationService.getLatestStatistics(resourceOid, objectClassName, task.getResult());
182+
183+
if (latestStatistics == null) {
184+
pageBase.warn("Statistics computation finished, but no statistics object was found.");
185+
target.add(pageBase.getFeedbackPanel());
186+
return;
187+
}
188+
189+
showStatisticsPopup(target, pageBase, latestStatistics, objectClassName);
190+
191+
} catch (CommonException e) {
192+
pageBase.error("Couldn't load regenerated statistics for " + objectClassName + ": " + e.getMessage());
193+
target.add(pageBase.getFeedbackPanel());
194+
} finally {
195+
task.getResult().computeStatusIfUnknown();
196+
}
197+
}
198+
199+
private @Nullable TaskType loadTask(@NotNull PageBase pageBase, @NotNull String taskOid) {
200+
Task task = pageBase.createSimpleTask(OP_LOAD_TASK);
201+
202+
PrismObject<TaskType> taskObject = WebModelServiceUtils.loadObject(
203+
TaskType.class, taskOid, null, true, pageBase, task, task.getResult());
204+
205+
return taskObject != null ? taskObject.asObjectable() : null;
206+
}
207+
}

0 commit comments

Comments
 (0)