dashboard
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,877 @@
|
||||
/* Calculated Fields Modal Styles */
|
||||
|
||||
.calculated-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #0072a0;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
border-color: #0072a0;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(0, 114, 160, 0.2);
|
||||
}
|
||||
|
||||
.field-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.field-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
margin: 4px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.field-tag:hover {
|
||||
background-color: #bbdefb;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
margin-right: 5px;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.equation-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.equation-hint {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.operators-container,
|
||||
.fields-container,
|
||||
.constants-container,
|
||||
.new-constant-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.subsection-icon {
|
||||
margin-right: 8px;
|
||||
color: #0072a0;
|
||||
}
|
||||
|
||||
.operator-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.operator-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #0072a0;
|
||||
color: white;
|
||||
border: 1px solid #005a80;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
.operator-tag:hover {
|
||||
background-color: #005a80;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.field-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.constant-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.constant-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: 1px solid #388e3c;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
margin: 4px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.constant-tag:hover {
|
||||
background-color: #388e3c;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.constant-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.add-constant-btn {
|
||||
background-color: #ff9800;
|
||||
border-color: #f57c00;
|
||||
}
|
||||
|
||||
.add-constant-btn:hover {
|
||||
background-color: #f57c00;
|
||||
}
|
||||
|
||||
.field-components-container {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.field-component-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.field-select-container,
|
||||
.constant-input-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field-select,
|
||||
.constant-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.remove-field-btn {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.add-field-btn {
|
||||
margin-top: 10px;
|
||||
background-color: #1976d2;
|
||||
border-color: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-field-btn:hover {
|
||||
background-color: #1565c0;
|
||||
}
|
||||
|
||||
.created-fields-section {
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.calculated-fields-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calculated-fields-table th {
|
||||
background-color: #f5f5f5;
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.calculated-fields-table td {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.field-name-cell {
|
||||
font-weight: 500;
|
||||
color: #0072a0;
|
||||
}
|
||||
|
||||
.operation-cell {
|
||||
font-family: monospace;
|
||||
background-color: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.expression-cell {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.delete-field-btn {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.field-component-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.field-select-container,
|
||||
.constant-input-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Group By Modal Styles */
|
||||
.field-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.field-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
margin: 4px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.field-tag:hover {
|
||||
background-color: #bbdefb;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.field-tag.selected {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
.selected-fields-container {
|
||||
min-height: 50px;
|
||||
padding: 10px;
|
||||
border: 1px dashed #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.selected-field-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
margin: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.selected-field-tag .btn-icon {
|
||||
margin-left: 8px;
|
||||
padding: 2px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.no-selection {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.aggregation-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.aggregation-row {
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.aggregation-row label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.clr-select {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Data Lake Type Styles */
|
||||
.clr-subtext {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
[data-multiple] {
|
||||
height: auto !important;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Checkbox Styles for Blending Data Lakes */
|
||||
.checkbox-container {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.checkbox-item label {
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Blending Keys Panel Styles */
|
||||
.blending-keys-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
border-top: 1px solid #ccc;
|
||||
box-shadow: 0 -2px 4px rgba(0,0,0,0.1);
|
||||
z-index: 1000;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.keys-container {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.keys-container h4 {
|
||||
margin-top: 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.headers-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.header-tag {
|
||||
display: inline-block;
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 12px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.sql-editor-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sql-textarea {
|
||||
width: 100%;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Blending Configuration Modal Styles */
|
||||
.blending-config-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #0072a0;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.keys-section {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.key-card {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.key-header h5 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.headers-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
margin: 4px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.header-tag:hover {
|
||||
background-color: #bbdefb;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.sql-editor-section {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.sql-editor-container {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sql-textarea {
|
||||
width: 100%;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.editor-hint {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Field Mapping Modal Styles */
|
||||
.field-mapping-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
margin: 0;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.mapping-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.mapping-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.mapping-table th {
|
||||
background-color: #f5f5f5;
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.mapping-table td {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.original-field {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.field-tag {
|
||||
display: inline-block;
|
||||
background-color: #e0e0e0;
|
||||
border: 1px solid #bdbdbd;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mapping-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mapping-input:focus {
|
||||
outline: none;
|
||||
border-color: #0072a0;
|
||||
box-shadow: 0 0 0 2px rgba(0, 114, 160, 0.2);
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Scheduler Modal Styles */
|
||||
.scheduler-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.data-lake-info h4 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.job-info-section {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.job-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.job-header h5 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-light {
|
||||
background-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.job-details {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.detail-row label {
|
||||
font-weight: 500;
|
||||
width: 120px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.detail-row span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.job-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.job-actions .btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.no-job-section {
|
||||
text-align: center;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.no-job-message p {
|
||||
margin: 10px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-job-message p:first-child {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.create-job-actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.create-job-actions .btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Webhook Status Styles */
|
||||
.webhook-enabled {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.webhook-disabled {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Webhook Status Styles */
|
||||
.webhook-status {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.webhook-status.enabled {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.webhook-status.disabled {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Webhook Modal Styles */
|
||||
.webhook-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.webhook-container .data-lake-info h4 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.webhook-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.webhook-info {
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.webhook-info p {
|
||||
margin: 0;
|
||||
color: #1976d2;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from "rxjs";
|
||||
import { HttpClient, HttpHeaders, HttpParams, } from "@angular/common/http";
|
||||
import { ApiRequestService } from "src/app/services/api/api-request.service";
|
||||
import { environment } from 'src/environments/environment';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class Data_lakeservice{
|
||||
private baseURL = "Data_lake/Data_lake" ; constructor(
|
||||
private http: HttpClient,
|
||||
private apiRequest: ApiRequestService,
|
||||
) { }
|
||||
getAll(page?: number, size?: number): Observable<any> {
|
||||
return this.apiRequest.get(this.baseURL);
|
||||
}
|
||||
getById(id: number): Observable<any> {
|
||||
const _http = this.baseURL + "/" + id;
|
||||
return this.apiRequest.get(_http);
|
||||
}
|
||||
create(data: any): Observable<any> {
|
||||
return this.apiRequest.post(this.baseURL, data);
|
||||
}
|
||||
update(id: number, data: any): Observable<any> {
|
||||
const _http = this.baseURL + "/" + id;
|
||||
return this.apiRequest.put(_http, data);
|
||||
}
|
||||
delete(id: number): Observable<any> {
|
||||
const _http = this.baseURL + "/" + id;
|
||||
return this.apiRequest.delete(_http);
|
||||
}
|
||||
|
||||
// New method for updating JSON
|
||||
updateJson(id: number): Observable<any> {
|
||||
const _http = this.baseURL + "/json/" + id;
|
||||
return this.apiRequest.put(_http, {});
|
||||
}
|
||||
|
||||
// Method to fetch available keys from API
|
||||
fetchAvailableKeys(url: string, sureId: number): Observable<string[]> {
|
||||
const apiUrl = `chart/getAllKeys?apiUrl=${encodeURIComponent(url)}&sureId=${sureId}`;
|
||||
return this.apiRequest.get(apiUrl);
|
||||
}
|
||||
|
||||
// Method to fetch blending keys for a specific lake ID
|
||||
fetchBlendingKeys(lakeId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/keys/${lakeId}`;
|
||||
return this.apiRequest.get(_http);
|
||||
}
|
||||
|
||||
// Method to update calculated fields for a data lake item
|
||||
updateCalculatedFields(id: number, calculatedFieldJson: string, isCalculatedField: boolean): Observable<any> {
|
||||
const _http = this.baseURL + "/" + id;
|
||||
const data = {
|
||||
calculated_field_json: calculatedFieldJson,
|
||||
iscalculatedfield: isCalculatedField
|
||||
};
|
||||
return this.apiRequest.put(_http, data);
|
||||
}
|
||||
|
||||
// Method to enable webhook for a data lake item
|
||||
enableWebhook(id: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/webhook/${id}`;
|
||||
return this.apiRequest.get(_http);
|
||||
}
|
||||
// updateaction
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const Data_lakecardvariable = {
|
||||
"cardButton": false,
|
||||
"cardmodeldata": ``
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<div class="cron-job-builder">
|
||||
<h4>Cron Job Builder</h4>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Second</label>
|
||||
<select class="form-control" [(ngModel)]="second" (change)="onFieldChange()">
|
||||
<option *ngFor="let option of secondOptions" [value]="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Minute</label>
|
||||
<select class="form-control" [(ngModel)]="minute" (change)="onFieldChange()">
|
||||
<option *ngFor="let option of minuteOptions" [value]="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Hour</label>
|
||||
<select class="form-control" [(ngModel)]="hour" (change)="onFieldChange()">
|
||||
<option *ngFor="let option of hourOptions" [value]="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Day of Month</label>
|
||||
<select class="form-control" [(ngModel)]="dayOfMonth" (change)="onFieldChange()">
|
||||
<option *ngFor="let option of dayOfMonthOptions" [value]="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Month</label>
|
||||
<select class="form-control" [(ngModel)]="month" (change)="onFieldChange()">
|
||||
<option *ngFor="let option of monthOptions" [value]="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Day of Week</label>
|
||||
<select class="form-control" [(ngModel)]="dayOfWeek" (change)="onFieldChange()">
|
||||
<option *ngFor="let option of dayOfWeekOptions" [value]="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-12">
|
||||
<div class="form-group">
|
||||
<label>Generated Cron Expression:</label>
|
||||
<input type="text" class="form-control" [value]="buildCronExpression()" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="cronDescription">
|
||||
<div class="clr-col-12">
|
||||
<div class="cron-description">
|
||||
<strong>Schedule Description:</strong> {{ cronDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
.cron-job-builder {
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f8f8f8;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 10px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
select, input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cron-description {
|
||||
padding: 10px;
|
||||
background-color: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
|
||||
strong {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cron-job-builder',
|
||||
templateUrl: './cron-job-builder.component.html',
|
||||
styleUrls: ['./cron-job-builder.component.scss']
|
||||
})
|
||||
export class CronJobBuilderComponent implements OnInit, OnChanges {
|
||||
@Input() cronExpression: string = '';
|
||||
@Input() instanceId: string = ''; // Unique identifier for each instance
|
||||
@Output() cronExpressionChange = new EventEmitter<string>();
|
||||
|
||||
// Cron job fields (now starting with seconds)
|
||||
second: string = '*';
|
||||
minute: string = '*';
|
||||
hour: string = '*';
|
||||
dayOfMonth: string = '*';
|
||||
month: string = '*';
|
||||
dayOfWeek: string = '*';
|
||||
|
||||
// Human readable description
|
||||
cronDescription: string = '';
|
||||
|
||||
// Options for selectors
|
||||
secondOptions: any[] = [];
|
||||
minuteOptions: any[] = [];
|
||||
hourOptions: any[] = [];
|
||||
dayOfMonthOptions: any[] = [];
|
||||
monthOptions: any[] = [];
|
||||
dayOfWeekOptions: any[] = [];
|
||||
|
||||
ngOnInit() {
|
||||
// Initialize options for each instance
|
||||
this.secondOptions = this.generateOptions(0, 59, 'second');
|
||||
this.minuteOptions = this.generateOptions(0, 59, 'minute');
|
||||
this.hourOptions = this.generateOptions(0, 23, 'hour');
|
||||
this.dayOfMonthOptions = this.generateOptions(1, 31, 'day');
|
||||
this.monthOptions = [
|
||||
{ value: '*', label: 'Every month' },
|
||||
{ value: '1', label: 'January' },
|
||||
{ value: '2', label: 'February' },
|
||||
{ value: '3', label: 'March' },
|
||||
{ value: '4', label: 'April' },
|
||||
{ value: '5', label: 'May' },
|
||||
{ value: '6', label: 'June' },
|
||||
{ value: '7', label: 'July' },
|
||||
{ value: '8', label: 'August' },
|
||||
{ value: '9', label: 'September' },
|
||||
{ value: '10', label: 'October' },
|
||||
{ value: '11', label: 'November' },
|
||||
{ value: '12', label: 'December' }
|
||||
];
|
||||
this.dayOfWeekOptions = [
|
||||
{ value: '*', label: 'Every day' },
|
||||
{ value: '0', label: 'Sunday' },
|
||||
{ value: '1', label: 'Monday' },
|
||||
{ value: '2', label: 'Tuesday' },
|
||||
{ value: '3', label: 'Wednesday' },
|
||||
{ value: '4', label: 'Thursday' },
|
||||
{ value: '5', label: 'Friday' },
|
||||
{ value: '6', label: 'Saturday' }
|
||||
];
|
||||
|
||||
if (this.cronExpression) {
|
||||
this.parseCronExpression(this.cronExpression);
|
||||
}
|
||||
// Generate initial description
|
||||
this.generateDescription();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
// When cronExpression input changes, update the component state
|
||||
if (changes['cronExpression'] && !changes['cronExpression'].firstChange) {
|
||||
this.parseCronExpression(changes['cronExpression'].currentValue);
|
||||
this.generateDescription();
|
||||
}
|
||||
}
|
||||
|
||||
generateOptions(start: number, end: number, type: string): any[] {
|
||||
const options = [{ value: '*', label: `Every ${type} (${start === 0 ? '0' : start}-${end})` }];
|
||||
for (let i = start; i <= end; i++) {
|
||||
options.push({ value: i.toString(), label: i.toString() });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
parseCronExpression(expression: string) {
|
||||
const parts = expression.split(' ');
|
||||
if (parts.length >= 6) { // Now expecting 6 parts with seconds
|
||||
this.second = parts[0];
|
||||
this.minute = parts[1];
|
||||
this.hour = parts[2];
|
||||
this.dayOfMonth = parts[3];
|
||||
this.month = parts[4];
|
||||
this.dayOfWeek = parts[5];
|
||||
} else if (parts.length >= 5) { // For backward compatibility with 5-part cron expressions
|
||||
// Default seconds to 0 if not provided
|
||||
this.second = '0';
|
||||
this.minute = parts[0];
|
||||
this.hour = parts[1];
|
||||
this.dayOfMonth = parts[2];
|
||||
this.month = parts[3];
|
||||
this.dayOfWeek = parts[4];
|
||||
}
|
||||
}
|
||||
|
||||
generateDescription() {
|
||||
let description = 'Runs ';
|
||||
|
||||
// Second description
|
||||
if (this.second === '*') {
|
||||
description += 'every second';
|
||||
} else if (this.second.includes('/')) {
|
||||
const interval = this.second.split('/')[1];
|
||||
description += `every ${interval} seconds`;
|
||||
} else {
|
||||
description += `at second ${this.second}`;
|
||||
}
|
||||
|
||||
// Minute description
|
||||
if (this.minute === '*') {
|
||||
if (this.second === '*') {
|
||||
description += '';
|
||||
} else {
|
||||
description += ' of every minute';
|
||||
}
|
||||
} else if (this.minute.includes('/')) {
|
||||
const interval = this.minute.split('/')[1];
|
||||
description += ` of every ${interval} minutes`;
|
||||
} else {
|
||||
description += ` at minute ${this.minute}`;
|
||||
}
|
||||
|
||||
// Hour description
|
||||
if (this.hour === '*') {
|
||||
if (this.minute === '*' && this.second === '*') {
|
||||
description += '';
|
||||
} else {
|
||||
description += ' past every hour';
|
||||
}
|
||||
} else if (this.hour.includes('/')) {
|
||||
const interval = this.hour.split('/')[1];
|
||||
description += ` past every ${interval} hours`;
|
||||
} else {
|
||||
description += ` past hour ${this.hour}`;
|
||||
}
|
||||
|
||||
// Day of month description
|
||||
if (this.dayOfMonth !== '*' && this.dayOfMonth !== '?') {
|
||||
description += ` on day ${this.dayOfMonth}`;
|
||||
}
|
||||
|
||||
// Month description
|
||||
if (this.month !== '*') {
|
||||
const monthNames = ['', 'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
if (this.month.match(/^\d+$/)) {
|
||||
description += ` in ${monthNames[parseInt(this.month)]}`;
|
||||
} else {
|
||||
description += ` in ${this.month}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Day of week description
|
||||
if (this.dayOfWeek !== '*' && this.dayOfWeek !== '?') {
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
if (this.dayOfWeek.match(/^\d+$/)) {
|
||||
description += ` on ${dayNames[parseInt(this.dayOfWeek)]}`;
|
||||
} else {
|
||||
description += ` on ${this.dayOfWeek}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Special case for simple patterns
|
||||
if (this.second === '0' && this.minute === '0' && this.hour === '0' && this.dayOfMonth === '1' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs at midnight on the first day of every month';
|
||||
} else if (this.second === '0' && this.minute === '0' && this.hour === '0' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs at midnight every day';
|
||||
} else if (this.second === '0' && this.minute === '0' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs at the top of every hour';
|
||||
} else if (this.second === '0' && this.minute === '*' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs at the top of every minute';
|
||||
} else if (this.second === '*' && this.minute === '*' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||
description = 'Runs every second';
|
||||
}
|
||||
|
||||
this.cronDescription = description;
|
||||
}
|
||||
|
||||
buildCronExpression() {
|
||||
const expression = `${this.second} ${this.minute} ${this.hour} ${this.dayOfMonth} ${this.month} ${this.dayOfWeek}`;
|
||||
this.cronExpressionChange.emit(expression);
|
||||
this.generateDescription(); // Update description when expression changes
|
||||
return expression;
|
||||
}
|
||||
|
||||
onFieldChange() {
|
||||
this.buildCronExpression();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from "rxjs";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ApiRequestService } from "src/app/services/api/api-request.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataLakeSchedulerService {
|
||||
private baseURL = "scheduler";
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private apiRequest: ApiRequestService,
|
||||
) { }
|
||||
|
||||
// Get job by lake ID
|
||||
getJobByLakeId(lakeId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/lake/${lakeId}`;
|
||||
return this.apiRequest.get(_http);
|
||||
}
|
||||
|
||||
// Create a new job
|
||||
createJob(jobData: any): Observable<any> {
|
||||
const _http = `${this.baseURL}/create`;
|
||||
return this.apiRequest.post(_http, jobData);
|
||||
}
|
||||
|
||||
// Pause a job
|
||||
pauseJob(jobId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/pause/${jobId}`;
|
||||
return this.apiRequest.post(_http, {});
|
||||
}
|
||||
|
||||
// Resume a job
|
||||
resumeJob(jobId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/resume/${jobId}`;
|
||||
return this.apiRequest.post(_http, {});
|
||||
}
|
||||
|
||||
// Stop a job
|
||||
stopJob(jobId: number): Observable<any> {
|
||||
const _http = `${this.baseURL}/stop/${jobId}`;
|
||||
return this.apiRequest.delete(_http);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,67 @@
|
||||
<ol class="breadcrumb breadcrumb-arrow font-trirong">
|
||||
<li><a href="javascript://" [routerLink]="['/cns-portal/dashboard/order']"><clr-icon shape="home"></clr-icon></a></li>
|
||||
<li><a href="javascript://"><clr-icon shape="dashboard"></clr-icon> {{ 'Dashboard_builder' | translate }}</a></li>
|
||||
</ol>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="dg-wrapper">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<h3>{{ 'Dashboard_builder' | translate }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-4" style="text-align: right;">
|
||||
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
||||
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
|
||||
</button>
|
||||
<button class="btn btn-outline" (click)="onExport()">
|
||||
<clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }}
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotoadd()">
|
||||
<clr-icon shape="plus"></clr-icon> {{ 'ADD' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ...... </clr-spinner></ng-template>
|
||||
<div *ngIf="error;else loadingSpinner">{{error}}</div></clr-dg-placeholder>
|
||||
|
||||
<clr-dg-column [clrDgField]="''">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Go_to' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'dashboard_name'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Dashboard_Name' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'description'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Description' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column >
|
||||
<clr-dg-column [clrDgField]="'secuirity_profile'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Security_Profile' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column >
|
||||
<clr-dg-column [clrDgField]="'add_to_home'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Add_to_home' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column >
|
||||
<!-- <clr-dg-column [clrDgField]="'formType'">
|
||||
<li><a href="javascript://" [routerLink]="['/cns-portal/dashboard/order']"><clr-icon shape="home"></clr-icon></a></li>
|
||||
<li><a href="javascript://"><clr-icon shape="dashboard"></clr-icon> {{ 'Dashboard_builder' | translate }}</a></li>
|
||||
</ol>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="dg-wrapper">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">x
|
||||
<h3>{{ 'Dashboard_builder' | translate }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-4" style="text-align: right;">
|
||||
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
|
||||
<clr-icon shape="shield"></clr-icon>Shield Dashboard
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
||||
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
|
||||
</button>
|
||||
<!-- Add Chart Config button -->
|
||||
<button class="btn btn-primary" (click)="openChartConfig()">
|
||||
<clr-icon shape="cog"></clr-icon> Chart Config
|
||||
</button>
|
||||
<button class="btn btn-outline" (click)="onExport()">
|
||||
<clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }}
|
||||
</button>
|
||||
<button id="add" class="btn btn-primary" (click)="gotoadd()">
|
||||
<clr-icon shape="plus"></clr-icon> {{ 'ADD' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ......
|
||||
</clr-spinner></ng-template>
|
||||
<div *ngIf="error;else loadingSpinner">{{error}}</div>
|
||||
</clr-dg-placeholder>
|
||||
|
||||
<clr-dg-column [clrDgField]="''">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Go_to' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'dashboard_name'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Dashboard_Name' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'description'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Description' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'secuirity_profile'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Security_Profile' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'add_to_home'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Add_to_home' | translate }}
|
||||
</ng-container>
|
||||
</clr-dg-column>
|
||||
<!-- <clr-dg-column [clrDgField]="'formType'">
|
||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
Build Status
|
||||
</ng-container>
|
||||
@@ -62,84 +71,86 @@
|
||||
Testing
|
||||
</ng-container>
|
||||
</clr-dg-column > -->
|
||||
<clr-dg-column [clrDgField]="'action'"> <ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Action' | translate }}
|
||||
</ng-container></clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let user of data?.slice()?.reverse();" [clrDgItem]="user">
|
||||
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;" (click)="goToEdit(user.id)"> {{ 'SET_UP' | translate }}</span></clr-dg-cell>
|
||||
<clr-dg-cell>{{user.dashboard_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.description}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.add_to_home}}</clr-dg-cell>
|
||||
<clr-dg-column [clrDgField]="'action'"> <ng-container *clrDgHideableColumn="{hidden: false}">
|
||||
{{ 'Action' | translate }}
|
||||
</ng-container></clr-dg-column>
|
||||
|
||||
<!-- <clr-dg-cell><input type="radio" id="cb1" class="dots" [ngStyle]="{'background-color': user.build == true ? 'green' : 'red'}"></clr-dg-cell>
|
||||
<clr-dg-row *clrDgItems="let user of data?.slice()?.reverse();" [clrDgItem]="user">
|
||||
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;"
|
||||
(click)="goToEdit(user.id)"> {{ 'SET_UP' | translate }}</span></clr-dg-cell>
|
||||
<clr-dg-cell>{{user.dashboard_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.description}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.add_to_home}}</clr-dg-cell>
|
||||
|
||||
<!-- <clr-dg-cell><input type="radio" id="cb1" class="dots" [ngStyle]="{'background-color': user.build == true ? 'green' : 'red'}"></clr-dg-cell>
|
||||
<clr-dg-cell>{{user.testing}}</clr-dg-cell> -->
|
||||
<clr-dg-cell>
|
||||
<!-- <span style="cursor: pointer; padding: 10px; "><clr-icon shape="form" (click)="goToEdit(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
|
||||
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true" class="tooltip tooltip-sm tooltip-top-left">
|
||||
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error" style="color: red;"></clr-icon></span>
|
||||
<span class="tooltip-content">{{ 'Delete' | translate }}</span>
|
||||
</a>
|
||||
|
||||
<clr-signpost>
|
||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||
<h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5>
|
||||
<div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div>
|
||||
<div>{{ 'Created_At' | translate }}: <code class="clr-code">{{ user.createdAt | date }}</code></div>
|
||||
<div>{{ 'Created_By' | translate }}: <code class="clr-code">{{ user.createdBy }}</code></div>
|
||||
<div>{{ 'Updated_At' | translate }}: <code class="clr-code">{{ user.updatedAt | date }}</code></div>
|
||||
<div>{{ 'Updated_By' | translate }}: <code class="clr-code">{{ user.updatedBy }}</code></div>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
|
||||
</clr-dg-cell>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="goToEditData(user.id)">{{ 'Edit' | translate }} <clr-icon shape="edit" class="is-error"></clr-icon></button>
|
||||
<!-- <button class="action-item" (click)="onDelete(user)">Delete<clr-icon shape="trash" class="is-error"></clr-icon></button> -->
|
||||
</clr-dg-action-overflow>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{ 'Users_per_page' | translate }}</clr-dg-page-size>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
{{ 'of' | translate }} {{pagination.totalItems}} {{ 'users' | translate }}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
<clr-dg-cell>
|
||||
<!-- <span style="cursor: pointer; padding: 10px; "><clr-icon shape="form" (click)="goToEdit(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
|
||||
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true"
|
||||
class="tooltip tooltip-sm tooltip-top-left">
|
||||
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error"
|
||||
style="color: red;"></clr-icon></span>
|
||||
<span class="tooltip-content">{{ 'Delete' | translate }}</span>
|
||||
</a>
|
||||
|
||||
<clr-signpost>
|
||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
|
||||
style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||
<h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5>
|
||||
<div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div>
|
||||
<div>{{ 'Created_At' | translate }}: <code class="clr-code">{{ user.createdAt | date }}</code></div>
|
||||
<div>{{ 'Created_By' | translate }}: <code class="clr-code">{{ user.createdBy }}</code></div>
|
||||
<div>{{ 'Updated_At' | translate }}: <code class="clr-code">{{ user.updatedAt | date }}</code></div>
|
||||
<div>{{ 'Updated_By' | translate }}: <code class="clr-code">{{ user.updatedBy }}</code></div>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
|
||||
</clr-dg-cell>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="goToEditData(user.id)">{{ 'Edit' | translate }} <clr-icon shape="edit"
|
||||
class="is-error"></clr-icon></button>
|
||||
<!-- <button class="action-item" (click)="onDelete(user)">Delete<clr-icon shape="trash" class="is-error"></clr-icon></button> -->
|
||||
</clr-dg-action-overflow>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{ 'Users_per_page' | translate }}</clr-dg-page-size>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
{{ 'of' | translate }} {{pagination.totalItems}} {{ 'users' | translate }}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||
<div class="modal-body">
|
||||
<div class="s-order-dash-pg">
|
||||
<div class="chart-box" id="word1" (click)="gotoadd()"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/fromscratch.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Start_from_scratch' | translate }}</b> </h5>
|
||||
</div>
|
||||
<div class="chart-box" id="word1"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/copytemplate.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Import_from_template' | translate }}</b> </h5>
|
||||
</div>
|
||||
<div class="chart-box" id="word1"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/database.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Import_from_public_project' | translate }}</b> </h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||
<div class="modal-body">
|
||||
<div class="s-order-dash-pg">
|
||||
<div class="chart-box" id="word1" (click)="gotoadd()"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/fromscratch.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Start_from_scratch' | translate }}</b> </h5>
|
||||
</div>
|
||||
<div class="chart-box" id="word1" ><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/copytemplate.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Import_from_template' | translate }}</b> </h5>
|
||||
</div>
|
||||
<div class="chart-box" id="word1"><br>
|
||||
<img style="margin: auto; display: block;" src="/assets/images/database.png" height="90" width="90">
|
||||
<h5 class="center"> <b>{{ 'Import_from_public_project' | translate }}</b> </h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||
<div class="modal-body" *ngIf="rowSelected.id">
|
||||
<h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1>
|
||||
<h2 class="heading">{{rowSelected.id}}</h2>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button>
|
||||
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{ 'Delete' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||
<div class="modal-body" *ngIf="rowSelected.id">
|
||||
<h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1>
|
||||
<h2 class="heading">{{rowSelected.id}}</h2>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button>
|
||||
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary">{{ 'Delete' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
@@ -24,6 +24,7 @@ export class AllnewdashComponent implements OnInit {
|
||||
projectname;
|
||||
projectId;
|
||||
error;
|
||||
chartConfigManagerOpen = false;
|
||||
constructor(
|
||||
private router : Router,
|
||||
private route: ActivatedRoute,private dashboardService : DashboardService,
|
||||
@@ -121,4 +122,9 @@ export class AllnewdashComponent implements OnInit {
|
||||
// this.router.navigate(['../editdashn'],{relativeTo:this.route});
|
||||
// }
|
||||
|
||||
// Add method to open chart configuration manager
|
||||
openChartConfig(): void {
|
||||
this.router.navigate(['../chart-types'],{relativeTo:this.route});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,630 @@
|
||||
<div class="chart-config-manager">
|
||||
<h2>Chart Configuration Manager</h2>
|
||||
|
||||
<!-- Chart Types Section -->
|
||||
<clr-tabs>
|
||||
<clr-tab>
|
||||
<button clrTabLink>Chart Types</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Chart Types</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddChartTypeForm = true">
|
||||
Add Chart Type
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Chart Type Form -->
|
||||
<div class="card-block" *ngIf="showAddChartTypeForm">
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="newChartType.name" name="chartTypeName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="newChartType.displayName" name="chartTypeDisplayName" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Description</label>
|
||||
<textarea clrTextarea [(ngModel)]="newChartType.description" name="chartTypeDescription"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Active</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newChartType.isActive" name="chartTypeIsActive" />
|
||||
<label>Active</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="createChartType()" [disabled]="!newChartType.name">Save</button>
|
||||
<button class="btn" (click)="showAddChartTypeForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Chart Types Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Name</clr-dg-column>
|
||||
<clr-dg-column>Display Name</clr-dg-column>
|
||||
<clr-dg-column>Description</clr-dg-column>
|
||||
<clr-dg-column>Status</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let chartType of chartTypes" [clrDgItem]="chartType">
|
||||
<clr-dg-cell>{{chartType.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.displayName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.description}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
|
||||
{{chartType.isActive ? 'Active' : 'Inactive'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectChartTypeForEdit(chartType)">
|
||||
<cds-icon shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteChartType(chartType.id)">
|
||||
<cds-icon shape="trash"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="onChartTypeSelect(chartType)">
|
||||
<cds-icon shape="eye"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{chartTypes.length}} chart types
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit Chart Type Form -->
|
||||
<div class="card-block" *ngIf="selectedChartType">
|
||||
<h4>Edit Chart Type</h4>
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedChartType.name" name="editChartTypeName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedChartType.displayName" name="editChartTypeDisplayName" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Description</label>
|
||||
<textarea clrTextarea [(ngModel)]="selectedChartType.description" name="editChartTypeDescription"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Active</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedChartType.isActive" name="editChartTypeIsActive" />
|
||||
<label>Active</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="updateChartType()" [disabled]="!selectedChartType.name">Update</button>
|
||||
<button class="btn" (click)="selectedChartType = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
|
||||
<!-- UI Components Section -->
|
||||
<clr-tab *ngIf="selectedChartType">
|
||||
<button clrTabLink>UI Components</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>UI Components for {{selectedChartType?.name}}</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddUiComponentForm = true">
|
||||
Add UI Component
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add UI Component Form -->
|
||||
<div class="card-block" *ngIf="showAddUiComponentForm">
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Component Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="newUiComponent.componentName" name="componentName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Component Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="newUiComponent.componentType" name="componentType" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="newUiComponent.displayLabel" name="displayLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Placeholder</label>
|
||||
<input clrInput type="text" [(ngModel)]="newUiComponent.placeholder" name="placeholder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="newUiComponent.sortOrder" name="sortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newUiComponent.isRequired" name="isRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="createUiComponent()" [disabled]="!newUiComponent.componentName">Save</button>
|
||||
<button class="btn" (click)="showAddUiComponentForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- UI Components Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Component Name</clr-dg-column>
|
||||
<clr-dg-column>Component Type</clr-dg-column>
|
||||
<clr-dg-column>Display Label</clr-dg-column>
|
||||
<clr-dg-column>Placeholder</clr-dg-column>
|
||||
<clr-dg-column>Required</clr-dg-column>
|
||||
<clr-dg-column>Sort Order</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let uiComponent of uiComponents" [clrDgItem]="uiComponent">
|
||||
<clr-dg-cell>{{uiComponent.componentName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{uiComponent.componentType}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{uiComponent.displayLabel}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{uiComponent.placeholder}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="uiComponent.isRequired" [class.label-danger]="!uiComponent.isRequired">
|
||||
{{uiComponent.isRequired ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{uiComponent.sortOrder}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectUiComponentForEdit(uiComponent)">
|
||||
<cds-icon shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteUiComponent(uiComponent.id)">
|
||||
<cds-icon shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{uiComponents.length}} UI components
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit UI Component Form -->
|
||||
<div class="card-block" *ngIf="selectedUiComponent">
|
||||
<h4>Edit UI Component</h4>
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Component Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentName" name="editComponentName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Component Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentType" name="editComponentType" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedUiComponent.displayLabel" name="editDisplayLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Placeholder</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedUiComponent.placeholder" name="editPlaceholder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="selectedUiComponent.sortOrder" name="editSortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedUiComponent.isRequired" name="editIsRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="updateUiComponent()" [disabled]="!selectedUiComponent.componentName">Update</button>
|
||||
<button class="btn" (click)="selectedUiComponent = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
|
||||
<!-- Component Properties Section -->
|
||||
<clr-tab *ngIf="selectedUiComponent">
|
||||
<button clrTabLink>Component Properties</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Properties for {{selectedUiComponent?.componentName}}</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddComponentPropertyForm = true">
|
||||
Add Property
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Component Property Form -->
|
||||
<div class="card-block" *ngIf="showAddComponentPropertyForm">
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Property Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyName" name="propertyName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Property Value</label>
|
||||
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyValue" name="propertyValue" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Property Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyType" name="propertyType" />
|
||||
</clr-input-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="createComponentProperty()" [disabled]="!newComponentProperty.propertyName">Save</button>
|
||||
<button class="btn" (click)="showAddComponentPropertyForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Component Properties Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Property Name</clr-dg-column>
|
||||
<clr-dg-column>Property Value</clr-dg-column>
|
||||
<clr-dg-column>Property Type</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let property of componentProperties" [clrDgItem]="property">
|
||||
<clr-dg-cell>{{property.propertyName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.propertyValue}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.propertyType}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectComponentPropertyForEdit(property)">
|
||||
<cds-icon shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteComponentProperty(property.id)">
|
||||
<cds-icon shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{componentProperties.length}} properties
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit Component Property Form -->
|
||||
<div class="card-block" *ngIf="selectedComponentProperty">
|
||||
<h4>Edit Component Property</h4>
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Property Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyName" name="editPropertyName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Property Value</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyValue" name="editPropertyValue" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Property Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyType" name="editPropertyType" />
|
||||
</clr-input-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="updateComponentProperty()" [disabled]="!selectedComponentProperty.propertyName">Update</button>
|
||||
<button class="btn" (click)="selectedComponentProperty = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
|
||||
<!-- Chart Templates Section -->
|
||||
<clr-tab *ngIf="selectedChartType">
|
||||
<button clrTabLink>Chart Templates</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Templates for {{selectedChartType?.name}}</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddChartTemplateForm = true">
|
||||
Add Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Chart Template Form -->
|
||||
<div class="card-block" *ngIf="showAddChartTemplateForm">
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Template Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="newChartTemplate.templateName" name="templateName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>HTML Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateHtml" name="templateHtml" rows="5"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>CSS Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateCss" name="templateCss" rows="5"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Default</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newChartTemplate.isDefault" name="isDefault" />
|
||||
<label>Default Template</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="createChartTemplate()" [disabled]="!newChartTemplate.templateName">Save</button>
|
||||
<button class="btn" (click)="showAddChartTemplateForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Chart Templates Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Template Name</clr-dg-column>
|
||||
<clr-dg-column>Is Default</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let template of chartTemplates" [clrDgItem]="template">
|
||||
<clr-dg-cell>{{template.templateName}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="template.isDefault" [class.label-danger]="!template.isDefault">
|
||||
{{template.isDefault ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectChartTemplateForEdit(template)">
|
||||
<cds-icon shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteChartTemplate(template.id)">
|
||||
<cds-icon shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{chartTemplates.length}} templates
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit Chart Template Form -->
|
||||
<div class="card-block" *ngIf="selectedChartTemplate">
|
||||
<h4>Edit Chart Template</h4>
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Template Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedChartTemplate.templateName" name="editTemplateName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>HTML Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateHtml" name="editTemplateHtml" rows="5"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>CSS Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateCss" name="editTemplateCss" rows="5"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Default</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedChartTemplate.isDefault" name="editIsDefault" />
|
||||
<label>Default Template</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="updateChartTemplate()" [disabled]="!selectedChartTemplate.templateName">Update</button>
|
||||
<button class="btn" (click)="selectedChartTemplate = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
|
||||
<!-- Dynamic Fields Section -->
|
||||
<clr-tab *ngIf="selectedChartType">
|
||||
<button clrTabLink>Dynamic Fields</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Dynamic Fields for {{selectedChartType?.name}}</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddDynamicFieldForm = true">
|
||||
Add Field
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Dynamic Field Form -->
|
||||
<div class="card-block" *ngIf="showAddDynamicFieldForm">
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Field Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldName" name="fieldName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldLabel" name="fieldLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldType" name="fieldType" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Field Options</label>
|
||||
<textarea clrTextarea [(ngModel)]="newDynamicField.fieldOptions" name="fieldOptions"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="newDynamicField.sortOrder" name="fieldSortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.isRequired" name="fieldIsRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Show in UI</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.showInUi" name="fieldShowInUi" />
|
||||
<label>Show in UI</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="createDynamicField()" [disabled]="!newDynamicField.fieldName">Save</button>
|
||||
<button class="btn" (click)="showAddDynamicFieldForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Fields Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Field Name</clr-dg-column>
|
||||
<clr-dg-column>Field Label</clr-dg-column>
|
||||
<clr-dg-column>Field Type</clr-dg-column>
|
||||
<clr-dg-column>Required</clr-dg-column>
|
||||
<clr-dg-column>Show in UI</clr-dg-column>
|
||||
<clr-dg-column>Sort Order</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let field of dynamicFields" [clrDgItem]="field">
|
||||
<clr-dg-cell>{{field.fieldName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{field.fieldLabel}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{field.fieldType}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="field.isRequired" [class.label-danger]="!field.isRequired">
|
||||
{{field.isRequired ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="field.showInUi" [class.label-danger]="!field.showInUi">
|
||||
{{field.showInUi ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{field.sortOrder}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectDynamicFieldForEdit(field)">
|
||||
<cds-icon shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteDynamicField(field.id)">
|
||||
<cds-icon shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{dynamicFields.length}} fields
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit Dynamic Field Form -->
|
||||
<div class="card-block" *ngIf="selectedDynamicField">
|
||||
<h4>Edit Dynamic Field</h4>
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Field Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldName" name="editFieldName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldLabel" name="editFieldLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldType" name="editFieldType" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Field Options</label>
|
||||
<textarea clrTextarea [(ngModel)]="selectedDynamicField.fieldOptions" name="editFieldOptions"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="selectedDynamicField.sortOrder" name="editFieldSortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.isRequired" name="editFieldIsRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Show in UI</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.showInUi" name="editFieldShowInUi" />
|
||||
<label>Show in UI</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="updateDynamicField()" [disabled]="!selectedDynamicField.fieldName">Update</button>
|
||||
<button class="btn" (click)="selectedDynamicField = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
</div>
|
||||
@@ -0,0 +1,60 @@
|
||||
.chart-config-manager {
|
||||
padding: 20px;
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-block {
|
||||
padding: 15px;
|
||||
|
||||
form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.label-success {
|
||||
background-color: #318700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.label-danger {
|
||||
background-color: #e62200;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,658 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
|
||||
interface ChartType {
|
||||
id: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface UiComponent {
|
||||
id: number;
|
||||
chartType: ChartType;
|
||||
componentName: string;
|
||||
componentType: string;
|
||||
displayLabel: string;
|
||||
placeholder: string;
|
||||
isRequired: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ComponentProperty {
|
||||
id: number;
|
||||
component: UiComponent;
|
||||
propertyName: string;
|
||||
propertyValue: string;
|
||||
propertyType: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ChartTemplate {
|
||||
id: number;
|
||||
chartType: ChartType;
|
||||
templateName: string;
|
||||
templateHtml: string;
|
||||
templateCss: string;
|
||||
isDefault: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface DynamicField {
|
||||
id: number;
|
||||
chartType: ChartType;
|
||||
fieldName: string;
|
||||
fieldLabel: string;
|
||||
fieldType: string;
|
||||
fieldOptions: string;
|
||||
isRequired: boolean;
|
||||
showInUi: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-config-manager',
|
||||
templateUrl: './chart-config-manager.component.html',
|
||||
styleUrls: ['./chart-config-manager.component.scss']
|
||||
})
|
||||
export class ChartConfigManagerComponent implements OnInit {
|
||||
// Chart Types
|
||||
chartTypes: ChartType[] = [];
|
||||
selectedChartType: ChartType | null = null;
|
||||
newChartType: Partial<ChartType> = {};
|
||||
showAddChartTypeForm = false;
|
||||
chartTypeLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// UI Components
|
||||
uiComponents: UiComponent[] = [];
|
||||
selectedUiComponent: UiComponent | null = null;
|
||||
newUiComponent: Partial<UiComponent> = {};
|
||||
showAddUiComponentForm = false;
|
||||
uiComponentLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// Component Properties
|
||||
componentProperties: ComponentProperty[] = [];
|
||||
selectedComponentProperty: ComponentProperty | null = null;
|
||||
newComponentProperty: Partial<ComponentProperty> = {};
|
||||
showAddComponentPropertyForm = false;
|
||||
componentPropertyLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// Chart Templates
|
||||
chartTemplates: ChartTemplate[] = [];
|
||||
selectedChartTemplate: ChartTemplate | null = null;
|
||||
newChartTemplate: Partial<ChartTemplate> = {};
|
||||
showAddChartTemplateForm = false;
|
||||
chartTemplateLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// Dynamic Fields
|
||||
dynamicFields: DynamicField[] = [];
|
||||
selectedDynamicField: DynamicField | null = null;
|
||||
newDynamicField: Partial<DynamicField> = {};
|
||||
showAddDynamicFieldForm = false;
|
||||
dynamicFieldLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// Error handling
|
||||
errorMessage: string | null = null;
|
||||
successMessage: string | null = null;
|
||||
|
||||
// API base URL
|
||||
private apiUrl = environment.apiUrl || 'http://localhost:8080/api';
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadChartTypes();
|
||||
}
|
||||
|
||||
// Show error message
|
||||
private showError(message: string): void {
|
||||
this.errorMessage = message;
|
||||
setTimeout(() => {
|
||||
this.errorMessage = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
private showSuccess(message: string): void {
|
||||
this.successMessage = message;
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Chart Type Methods
|
||||
loadChartTypes(): void {
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.get<ChartType[]>(`${this.apiUrl}/chart-types`).subscribe({
|
||||
next: (data) => {
|
||||
this.chartTypes = data;
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart types:', error);
|
||||
this.showError('Error loading chart types: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createChartType(): void {
|
||||
if (!this.newChartType.name) {
|
||||
this.showError('Chart type name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.post<ChartType>(`${this.apiUrl}/chart-types`, this.newChartType).subscribe({
|
||||
next: (data) => {
|
||||
this.chartTypes.push(data);
|
||||
this.newChartType = {};
|
||||
this.showAddChartTypeForm = false;
|
||||
this.showSuccess('Chart type created successfully');
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating chart type:', error);
|
||||
this.showError('Error creating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateChartType(): void {
|
||||
if (!this.selectedChartType || !this.selectedChartType.name) {
|
||||
this.showError('Chart type name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.put<ChartType>(`${this.apiUrl}/chart-types/${this.selectedChartType.id}`, this.selectedChartType).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.chartTypes.findIndex(ct => ct.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.chartTypes[index] = data;
|
||||
}
|
||||
this.selectedChartType = null;
|
||||
this.showSuccess('Chart type updated successfully');
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating chart type:', error);
|
||||
this.showError('Error updating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteChartType(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this chart type?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.delete(`${this.apiUrl}/chart-types/${id}`).subscribe({
|
||||
next: () => {
|
||||
this.chartTypes = this.chartTypes.filter(ct => ct.id !== id);
|
||||
// Also clear related data if the deleted chart type was selected
|
||||
if (this.selectedChartType && this.selectedChartType.id === id) {
|
||||
this.selectedChartType = null;
|
||||
this.uiComponents = [];
|
||||
this.chartTemplates = [];
|
||||
this.dynamicFields = [];
|
||||
}
|
||||
this.showSuccess('Chart type deleted successfully');
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting chart type:', error);
|
||||
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectChartTypeForEdit(chartType: ChartType): void {
|
||||
this.selectedChartType = { ...chartType };
|
||||
}
|
||||
|
||||
// UI Component Methods
|
||||
loadUiComponents(chartTypeId: number): void {
|
||||
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.get<UiComponent[]>(`${this.apiUrl}/ui-components/chart-type/${chartTypeId}`).subscribe({
|
||||
next: (data) => {
|
||||
this.uiComponents = data;
|
||||
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading UI components:', error);
|
||||
this.showError('Error loading UI components: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createUiComponent(): void {
|
||||
if (!this.selectedChartType) {
|
||||
this.showError('Please select a chart type first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newUiComponent.componentName) {
|
||||
this.showError('Component name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||
const uiComponentData = {
|
||||
...this.newUiComponent,
|
||||
chartType: { id: this.selectedChartType.id }
|
||||
};
|
||||
|
||||
this.http.post<UiComponent>(`${this.apiUrl}/ui-components`, uiComponentData).subscribe({
|
||||
next: (data) => {
|
||||
this.uiComponents.push(data);
|
||||
this.newUiComponent = {};
|
||||
this.showAddUiComponentForm = false;
|
||||
this.showSuccess('UI component created successfully');
|
||||
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating UI component:', error);
|
||||
this.showError('Error creating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateUiComponent(): void {
|
||||
if (!this.selectedUiComponent || !this.selectedUiComponent.componentName) {
|
||||
this.showError('Component name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.put<UiComponent>(`${this.apiUrl}/ui-components/${this.selectedUiComponent.id}`, this.selectedUiComponent).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.uiComponents.findIndex(uc => uc.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.uiComponents[index] = data;
|
||||
}
|
||||
this.selectedUiComponent = null;
|
||||
this.showSuccess('UI component updated successfully');
|
||||
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating UI component:', error);
|
||||
this.showError('Error updating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteUiComponent(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this UI component?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.delete(`${this.apiUrl}/ui-components/${id}`).subscribe({
|
||||
next: () => {
|
||||
this.uiComponents = this.uiComponents.filter(uc => uc.id !== id);
|
||||
if (this.selectedUiComponent && this.selectedUiComponent.id === id) {
|
||||
this.selectedUiComponent = null;
|
||||
}
|
||||
this.showSuccess('UI component deleted successfully');
|
||||
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting UI component:', error);
|
||||
this.showError('Error deleting UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectUiComponentForEdit(uiComponent: UiComponent): void {
|
||||
this.selectedUiComponent = { ...uiComponent };
|
||||
}
|
||||
|
||||
// Component Property Methods
|
||||
loadComponentProperties(componentId: number): void {
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.get<ComponentProperty[]>(`${this.apiUrl}/component-properties/component/${componentId}`).subscribe({
|
||||
next: (data) => {
|
||||
this.componentProperties = data;
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading component properties:', error);
|
||||
this.showError('Error loading component properties: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createComponentProperty(): void {
|
||||
if (!this.selectedUiComponent) {
|
||||
this.showError('Please select a UI component first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newComponentProperty.propertyName) {
|
||||
this.showError('Property name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
const componentPropertyData = {
|
||||
...this.newComponentProperty,
|
||||
component: { id: this.selectedUiComponent.id }
|
||||
};
|
||||
|
||||
this.http.post<ComponentProperty>(`${this.apiUrl}/component-properties`, componentPropertyData).subscribe({
|
||||
next: (data) => {
|
||||
this.componentProperties.push(data);
|
||||
this.newComponentProperty = {};
|
||||
this.showAddComponentPropertyForm = false;
|
||||
this.showSuccess('Component property created successfully');
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating component property:', error);
|
||||
this.showError('Error creating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateComponentProperty(): void {
|
||||
if (!this.selectedComponentProperty || !this.selectedComponentProperty.propertyName) {
|
||||
this.showError('Property name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.put<ComponentProperty>(`${this.apiUrl}/component-properties/${this.selectedComponentProperty.id}`, this.selectedComponentProperty).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.componentProperties.findIndex(cp => cp.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.componentProperties[index] = data;
|
||||
}
|
||||
this.selectedComponentProperty = null;
|
||||
this.showSuccess('Component property updated successfully');
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating component property:', error);
|
||||
this.showError('Error updating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteComponentProperty(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this component property?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.delete(`${this.apiUrl}/component-properties/${id}`).subscribe({
|
||||
next: () => {
|
||||
this.componentProperties = this.componentProperties.filter(cp => cp.id !== id);
|
||||
if (this.selectedComponentProperty && this.selectedComponentProperty.id === id) {
|
||||
this.selectedComponentProperty = null;
|
||||
}
|
||||
this.showSuccess('Component property deleted successfully');
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting component property:', error);
|
||||
this.showError('Error deleting component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectComponentPropertyForEdit(componentProperty: ComponentProperty): void {
|
||||
this.selectedComponentProperty = { ...componentProperty };
|
||||
}
|
||||
|
||||
// Chart Template Methods
|
||||
loadChartTemplates(chartTypeId: number): void {
|
||||
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.get<ChartTemplate[]>(`${this.apiUrl}/chart-templates/chart-type/${chartTypeId}`).subscribe({
|
||||
next: (data) => {
|
||||
this.chartTemplates = data;
|
||||
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart templates:', error);
|
||||
this.showError('Error loading chart templates: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createChartTemplate(): void {
|
||||
if (!this.selectedChartType) {
|
||||
this.showError('Please select a chart type first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newChartTemplate.templateName) {
|
||||
this.showError('Template name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||
const chartTemplateData = {
|
||||
...this.newChartTemplate,
|
||||
chartType: { id: this.selectedChartType.id }
|
||||
};
|
||||
|
||||
this.http.post<ChartTemplate>(`${this.apiUrl}/chart-templates`, chartTemplateData).subscribe({
|
||||
next: (data) => {
|
||||
this.chartTemplates.push(data);
|
||||
this.newChartTemplate = {};
|
||||
this.showAddChartTemplateForm = false;
|
||||
this.showSuccess('Chart template created successfully');
|
||||
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating chart template:', error);
|
||||
this.showError('Error creating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateChartTemplate(): void {
|
||||
if (!this.selectedChartTemplate || !this.selectedChartTemplate.templateName) {
|
||||
this.showError('Template name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.put<ChartTemplate>(`${this.apiUrl}/chart-templates/${this.selectedChartTemplate.id}`, this.selectedChartTemplate).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.chartTemplates.findIndex(ct => ct.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.chartTemplates[index] = data;
|
||||
}
|
||||
this.selectedChartTemplate = null;
|
||||
this.showSuccess('Chart template updated successfully');
|
||||
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating chart template:', error);
|
||||
this.showError('Error updating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteChartTemplate(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this chart template?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.delete(`${this.apiUrl}/chart-templates/${id}`).subscribe({
|
||||
next: () => {
|
||||
this.chartTemplates = this.chartTemplates.filter(ct => ct.id !== id);
|
||||
if (this.selectedChartTemplate && this.selectedChartTemplate.id === id) {
|
||||
this.selectedChartTemplate = null;
|
||||
}
|
||||
this.showSuccess('Chart template deleted successfully');
|
||||
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting chart template:', error);
|
||||
this.showError('Error deleting chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectChartTemplateForEdit(chartTemplate: ChartTemplate): void {
|
||||
this.selectedChartTemplate = { ...chartTemplate };
|
||||
}
|
||||
|
||||
// Dynamic Field Methods
|
||||
loadDynamicFields(chartTypeId: number): void {
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.get<DynamicField[]>(`${this.apiUrl}/dynamic-fields/chart-type/${chartTypeId}`).subscribe({
|
||||
next: (data) => {
|
||||
this.dynamicFields = data;
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading dynamic fields:', error);
|
||||
this.showError('Error loading dynamic fields: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createDynamicField(): void {
|
||||
if (!this.selectedChartType) {
|
||||
this.showError('Please select a chart type first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newDynamicField.fieldName) {
|
||||
this.showError('Field name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||
const dynamicFieldData = {
|
||||
...this.newDynamicField,
|
||||
chartType: { id: this.selectedChartType.id }
|
||||
};
|
||||
|
||||
this.http.post<DynamicField>(`${this.apiUrl}/dynamic-fields`, dynamicFieldData).subscribe({
|
||||
next: (data) => {
|
||||
this.dynamicFields.push(data);
|
||||
this.newDynamicField = {};
|
||||
this.showAddDynamicFieldForm = false;
|
||||
this.showSuccess('Dynamic field created successfully');
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating dynamic field:', error);
|
||||
this.showError('Error creating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDynamicField(): void {
|
||||
if (!this.selectedDynamicField || !this.selectedDynamicField.fieldName) {
|
||||
this.showError('Field name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.put<DynamicField>(`${this.apiUrl}/dynamic-fields/${this.selectedDynamicField.id}`, this.selectedDynamicField).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.dynamicFields.findIndex(df => df.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.dynamicFields[index] = data;
|
||||
}
|
||||
this.selectedDynamicField = null;
|
||||
this.showSuccess('Dynamic field updated successfully');
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating dynamic field:', error);
|
||||
this.showError('Error updating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteDynamicField(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this dynamic field?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||
this.http.delete(`${this.apiUrl}/dynamic-fields/${id}`).subscribe({
|
||||
next: () => {
|
||||
this.dynamicFields = this.dynamicFields.filter(df => df.id !== id);
|
||||
if (this.selectedDynamicField && this.selectedDynamicField.id === id) {
|
||||
this.selectedDynamicField = null;
|
||||
}
|
||||
this.showSuccess('Dynamic field deleted successfully');
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting dynamic field:', error);
|
||||
this.showError('Error deleting dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectDynamicFieldForEdit(dynamicField: DynamicField): void {
|
||||
this.selectedDynamicField = { ...dynamicField };
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
onChartTypeSelect(chartType: ChartType): void {
|
||||
this.selectedChartType = chartType;
|
||||
this.loadUiComponents(chartType.id);
|
||||
this.loadChartTemplates(chartType.id);
|
||||
this.loadDynamicFields(chartType.id);
|
||||
}
|
||||
|
||||
resetForms(): void {
|
||||
this.newChartType = {};
|
||||
this.newUiComponent = {};
|
||||
this.newComponentProperty = {};
|
||||
this.newChartTemplate = {};
|
||||
this.newDynamicField = {};
|
||||
this.showAddChartTypeForm = false;
|
||||
this.showAddUiComponentForm = false;
|
||||
this.showAddComponentPropertyForm = false;
|
||||
this.showAddChartTemplateForm = false;
|
||||
this.showAddDynamicFieldForm = false;
|
||||
this.selectedChartType = null;
|
||||
this.selectedUiComponent = null;
|
||||
this.selectedComponentProperty = null;
|
||||
this.selectedChartTemplate = null;
|
||||
this.selectedDynamicField = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
<div class="chart-config-manager">
|
||||
<h2>Chart Configuration Manager</h2>
|
||||
|
||||
<!-- Test message to verify component is loading -->
|
||||
<div class="alert alert-info">
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">Chart Config Manager Component Loaded Successfully</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error and Success Messages -->
|
||||
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" *ngIf="successMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Types Section -->
|
||||
<clr-tab>
|
||||
<button clrTabLink>Chart Types</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Chart Types</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddChartTypeForm = true">
|
||||
<cds-icon shape="plus"></cds-icon> Add Chart Type
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Chart Type Form -->
|
||||
<div class="card-block" *ngIf="showAddChartTypeForm">
|
||||
<app-chart-type-form
|
||||
[chartType]="newChartType"
|
||||
(save)="createChartType()"
|
||||
(cancel)="showAddChartTypeForm = false">
|
||||
</app-chart-type-form>
|
||||
</div>
|
||||
|
||||
<!-- Chart Types Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Name</clr-dg-column>
|
||||
<clr-dg-column>Display Name</clr-dg-column>
|
||||
<clr-dg-column>Description</clr-dg-column>
|
||||
<clr-dg-column>Status</clr-dg-column>
|
||||
<clr-dg-column>Created At</clr-dg-column>
|
||||
<clr-dg-column>Updated At</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let chartType of chartTypes" [clrDgItem]="chartType">
|
||||
<clr-dg-cell>{{chartType.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.displayName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.description}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
|
||||
{{chartType.isActive ? 'Active' : 'Inactive'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.createdAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.updatedAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectChartTypeForEdit(chartType)" title="Edit" [disabled]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="chartTypeLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="chartTypeLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteChartType(chartType.id)" title="Delete" [disabled]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="chartTypeLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="chartTypeLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="onChartTypeSelect(chartType)" title="View Details" [disabled]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="chartTypeLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="chartTypeLoadingState !== ClrLoadingState.LOADING" shape="eye"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{chartTypes.length}} chart type(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit Chart Type Form -->
|
||||
<div class="card-block" *ngIf="selectedChartType">
|
||||
<h4>Edit Chart Type</h4>
|
||||
<app-chart-type-form
|
||||
[chartType]="selectedChartType"
|
||||
[isEdit]="true"
|
||||
(save)="updateChartType()"
|
||||
(cancel)="selectedChartType = null">
|
||||
</app-chart-type-form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
|
||||
<!-- UI Components Section -->
|
||||
<clr-tab *ngIf="selectedChartType">
|
||||
<button clrTabLink>UI Components</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>UI Components for {{selectedChartType?.name}}</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddUiComponentForm = true">
|
||||
<cds-icon shape="plus"></cds-icon> Add UI Component
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add UI Component Form -->
|
||||
<div class="card-block" *ngIf="showAddUiComponentForm">
|
||||
<app-ui-component-form
|
||||
[uiComponent]="newUiComponent"
|
||||
(save)="createUiComponent()"
|
||||
(cancel)="showAddUiComponentForm = false">
|
||||
</app-ui-component-form>
|
||||
</div>
|
||||
|
||||
<!-- UI Components Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="uiComponentLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Component Name</clr-dg-column>
|
||||
<clr-dg-column>Component Type</clr-dg-column>
|
||||
<clr-dg-column>Display Label</clr-dg-column>
|
||||
<clr-dg-column>Required</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let uiComponent of uiComponents" [clrDgItem]="uiComponent">
|
||||
<clr-dg-cell>{{uiComponent.componentName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{uiComponent.componentType}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{uiComponent.displayLabel}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="uiComponent.isRequired" [class.label-danger]="!uiComponent.isRequired">
|
||||
{{uiComponent.isRequired ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell class="action-cell">
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-sm btn-icon" (click)="selectUiComponentForEdit(uiComponent)" title="Edit" [disabled]="uiComponentLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="uiComponentLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="uiComponentLoadingState !== ClrLoadingState.LOADING" shape="pencil" aria-label="Edit"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteUiComponent(uiComponent.id)" title="Delete" [disabled]="uiComponentLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="uiComponentLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="uiComponentLoadingState !== ClrLoadingState.LOADING" shape="trash" aria-label="Delete"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="onUiComponentSelect(uiComponent)" title="View Properties" [disabled]="uiComponentLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="uiComponentLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="uiComponentLoadingState !== ClrLoadingState.LOADING" shape="eye" aria-label="View Properties"></cds-icon>
|
||||
</button>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{uiComponents.length}} UI component(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit UI Component Form -->
|
||||
<div class="card-block" *ngIf="selectedUiComponent">
|
||||
<h4>Edit UI Component</h4>
|
||||
<app-ui-component-form
|
||||
[uiComponent]="selectedUiComponent"
|
||||
[isEdit]="true"
|
||||
(save)="updateUiComponent()"
|
||||
(cancel)="selectedUiComponent = null">
|
||||
</app-ui-component-form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
|
||||
<!-- Component Properties Section -->
|
||||
<clr-tab *ngIf="selectedUiComponent">
|
||||
<button clrTabLink>Component Properties</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Properties for {{selectedUiComponent?.componentName}}</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddComponentPropertyForm = true">
|
||||
<cds-icon shape="plus"></cds-icon> Add Property
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Component Property Form -->
|
||||
<div class="card-block" *ngIf="showAddComponentPropertyForm">
|
||||
<app-component-property-form
|
||||
[componentProperty]="newComponentProperty"
|
||||
(save)="createComponentProperty()"
|
||||
(cancel)="showAddComponentPropertyForm = false">
|
||||
</app-component-property-form>
|
||||
</div>
|
||||
|
||||
<!-- Component Properties Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Property Name</clr-dg-column>
|
||||
<clr-dg-column>Property Value</clr-dg-column>
|
||||
<clr-dg-column>Property Type</clr-dg-column>
|
||||
<clr-dg-column>Created At</clr-dg-column>
|
||||
<clr-dg-column>Updated At</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let property of componentProperties" [clrDgItem]="property">
|
||||
<clr-dg-cell>{{property.propertyName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.propertyValue}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.propertyType}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.createdAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.updatedAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectComponentPropertyForEdit(property)" title="Edit" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteComponentProperty(property.id)" title="Delete" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{componentProperties.length}} propertie(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit Component Property Form -->
|
||||
<div class="card-block" *ngIf="selectedComponentProperty">
|
||||
<h4>Edit Component Property</h4>
|
||||
<app-component-property-form
|
||||
[componentProperty]="selectedComponentProperty"
|
||||
[isEdit]="true"
|
||||
(save)="updateComponentProperty()"
|
||||
(cancel)="selectedComponentProperty = null">
|
||||
</app-component-property-form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
|
||||
<!-- Chart Templates Section -->
|
||||
<clr-tab *ngIf="selectedChartType">
|
||||
<button clrTabLink>Chart Templates</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Templates for {{selectedChartType?.name}}</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddChartTemplateForm = true">
|
||||
<cds-icon shape="plus"></cds-icon> Add Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Chart Template Form -->
|
||||
<div class="card-block" *ngIf="showAddChartTemplateForm">
|
||||
<app-chart-template-form
|
||||
[chartTemplate]="newChartTemplate"
|
||||
(save)="createChartTemplate()"
|
||||
(cancel)="showAddChartTemplateForm = false">
|
||||
</app-chart-template-form>
|
||||
</div>
|
||||
|
||||
<!-- Chart Templates Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="chartTemplateLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Template Name</clr-dg-column>
|
||||
<clr-dg-column>Is Default</clr-dg-column>
|
||||
<clr-dg-column>Created At</clr-dg-column>
|
||||
<clr-dg-column>Updated At</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let template of chartTemplates" [clrDgItem]="template">
|
||||
<clr-dg-cell>{{template.templateName}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="template.isDefault" [class.label-danger]="!template.isDefault">
|
||||
{{template.isDefault ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{template.createdAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{template.updatedAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectChartTemplateForEdit(template)" title="Edit" [disabled]="chartTemplateLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="chartTemplateLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="chartTemplateLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteChartTemplate(template.id)" title="Delete" [disabled]="chartTemplateLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="chartTemplateLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="chartTemplateLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{chartTemplates.length}} template(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit Chart Template Form -->
|
||||
<div class="card-block" *ngIf="selectedChartTemplate">
|
||||
<h4>Edit Chart Template</h4>
|
||||
<app-chart-template-form
|
||||
[chartTemplate]="selectedChartTemplate"
|
||||
[isEdit]="true"
|
||||
(save)="updateChartTemplate()"
|
||||
(cancel)="selectedChartTemplate = null">
|
||||
</app-chart-template-form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
|
||||
<!-- Dynamic Fields Section -->
|
||||
<clr-tab *ngIf="selectedChartType">
|
||||
<button clrTabLink>Dynamic Fields</button>
|
||||
<clr-tab-content>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Dynamic Fields for {{selectedChartType?.name}}</h3>
|
||||
<button class="btn btn-sm btn-primary" (click)="showAddDynamicFieldForm = true">
|
||||
<cds-icon shape="plus"></cds-icon> Add Field
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Dynamic Field Form -->
|
||||
<div class="card-block" *ngIf="showAddDynamicFieldForm">
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Field Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldName" name="fieldName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldLabel" name="fieldLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldType" name="fieldType" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Field Options</label>
|
||||
<textarea clrTextarea [(ngModel)]="newDynamicField.fieldOptions" name="fieldOptions"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="newDynamicField.sortOrder" name="fieldSortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.isRequired" name="fieldIsRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Show in UI</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.showInUi" name="fieldShowInUi" />
|
||||
<label>Show in UI</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="createDynamicField()" [disabled]="!newDynamicField.fieldName || dynamicFieldLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="dynamicFieldLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Save
|
||||
</button>
|
||||
<button class="btn" (click)="showAddDynamicFieldForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Fields Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="dynamicFieldLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Field Name</clr-dg-column>
|
||||
<clr-dg-column>Field Label</clr-dg-column>
|
||||
<clr-dg-column>Field Type</clr-dg-column>
|
||||
<clr-dg-column>Required</clr-dg-column>
|
||||
<clr-dg-column>Show in UI</clr-dg-column>
|
||||
<clr-dg-column>Sort Order</clr-dg-column>
|
||||
<clr-dg-column>Created At</clr-dg-column>
|
||||
<clr-dg-column>Updated At</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let field of dynamicFields" [clrDgItem]="field">
|
||||
<clr-dg-cell>{{field.fieldName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{field.fieldLabel}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{field.fieldType}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="field.isRequired" [class.label-danger]="!field.isRequired">
|
||||
{{field.isRequired ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="field.showInUi" [class.label-danger]="!field.showInUi">
|
||||
{{field.showInUi ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{field.sortOrder}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{field.createdAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{field.updatedAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectDynamicFieldForEdit(field)" title="Edit">
|
||||
<cds-icon shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteDynamicField(field.id)" title="Delete">
|
||||
<cds-icon shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{dynamicFields.length}} field(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit Dynamic Field Form -->
|
||||
<div class="card-block" *ngIf="selectedDynamicField">
|
||||
<h4>Edit Dynamic Field</h4>
|
||||
<form clrForm>
|
||||
<clr-input-container>
|
||||
<label>Field Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldName" name="editFieldName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldLabel" name="editFieldLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldType" name="editFieldType" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Field Options</label>
|
||||
<textarea clrTextarea [(ngModel)]="selectedDynamicField.fieldOptions" name="editFieldOptions"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="selectedDynamicField.sortOrder" name="editFieldSortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.isRequired" name="editFieldIsRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Show in UI</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.showInUi" name="editFieldShowInUi" />
|
||||
<label>Show in UI</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" (click)="updateDynamicField()" [disabled]="!selectedDynamicField.fieldName || dynamicFieldLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="dynamicFieldLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Update
|
||||
</button>
|
||||
<button class="btn" (click)="selectedDynamicField = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
</div>
|
||||
@@ -0,0 +1,97 @@
|
||||
.chart-config-manager {
|
||||
padding: 20px;
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-block {
|
||||
padding: 15px;
|
||||
|
||||
form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.label-success {
|
||||
background-color: #318700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.label-danger {
|
||||
background-color: #e62200;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
clr-tab-content {
|
||||
padding: 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-buttons .btn-icon {
|
||||
min-width: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// Ensure icons are visible
|
||||
cds-icon {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
clr-spinner {
|
||||
margin: 0;
|
||||
}cds-icon {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
clr-spinner {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { ChartTypeService } from '../chart-type-manager/chart-type.service';
|
||||
import { UiComponentService } from './ui-component.service';
|
||||
import { ComponentPropertyService } from './component-property.service';
|
||||
import { ChartTemplateService } from './chart-template.service';
|
||||
import { DynamicFieldService } from './dynamic-field.service';
|
||||
|
||||
export interface ChartType {
|
||||
id: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UiComponent {
|
||||
id: number;
|
||||
chartType: ChartType;
|
||||
componentName: string;
|
||||
componentType: string;
|
||||
displayLabel: string;
|
||||
placeholder: string;
|
||||
isRequired: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ComponentProperty {
|
||||
id: number;
|
||||
component: UiComponent;
|
||||
propertyName: string;
|
||||
propertyValue: string;
|
||||
propertyType: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ChartTemplate {
|
||||
id: number;
|
||||
chartType: ChartType;
|
||||
templateName: string;
|
||||
templateHtml: string;
|
||||
templateCss: string;
|
||||
isDefault: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DynamicField {
|
||||
id: number;
|
||||
chartType: ChartType;
|
||||
fieldName: string;
|
||||
fieldLabel: string;
|
||||
fieldType: string;
|
||||
fieldOptions: string;
|
||||
isRequired: boolean;
|
||||
showInUi: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-config-manager',
|
||||
templateUrl: './chart-config-manager.component.html',
|
||||
styleUrls: ['./chart-config-manager.component.scss']
|
||||
})
|
||||
export class ChartConfigManagerComponent implements OnInit {
|
||||
// Chart Types
|
||||
chartTypes: ChartType[] = [];
|
||||
selectedChartType: ChartType | null = null;
|
||||
newChartType: Partial<ChartType> = {};
|
||||
showAddChartTypeForm = false;
|
||||
chartTypeLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// UI Components
|
||||
uiComponents: UiComponent[] = [];
|
||||
selectedUiComponent: UiComponent | null = null;
|
||||
newUiComponent: Partial<UiComponent> = {};
|
||||
showAddUiComponentForm = false;
|
||||
uiComponentLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// Component Properties
|
||||
componentProperties: ComponentProperty[] = [];
|
||||
selectedComponentProperty: ComponentProperty | null = null;
|
||||
newComponentProperty: Partial<ComponentProperty> = {};
|
||||
showAddComponentPropertyForm = false;
|
||||
componentPropertyLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// Chart Templates
|
||||
chartTemplates: ChartTemplate[] = [];
|
||||
selectedChartTemplate: ChartTemplate | null = null;
|
||||
newChartTemplate: Partial<ChartTemplate> = {};
|
||||
showAddChartTemplateForm = false;
|
||||
chartTemplateLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// Dynamic Fields
|
||||
dynamicFields: DynamicField[] = [];
|
||||
selectedDynamicField: DynamicField | null = null;
|
||||
newDynamicField: Partial<DynamicField> = {};
|
||||
showAddDynamicFieldForm = false;
|
||||
dynamicFieldLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// Make ClrLoadingState available to template
|
||||
readonly ClrLoadingState = ClrLoadingState;
|
||||
|
||||
// Error handling
|
||||
errorMessage: string | null = null;
|
||||
successMessage: string | null = null;
|
||||
|
||||
constructor(
|
||||
private chartTypeService: ChartTypeService,
|
||||
private uiComponentService: UiComponentService,
|
||||
private componentPropertyService: ComponentPropertyService,
|
||||
private chartTemplateService: ChartTemplateService,
|
||||
private dynamicFieldService: DynamicFieldService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log('ChartConfigManagerComponent initialized');
|
||||
this.loadChartTypes();
|
||||
}
|
||||
|
||||
// Show error message
|
||||
private showError(message: string): void {
|
||||
this.errorMessage = message;
|
||||
setTimeout(() => {
|
||||
this.errorMessage = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
private showSuccess(message: string): void {
|
||||
this.successMessage = message;
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Chart Type Methods
|
||||
loadChartTypes(): void {
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
console.log('Loading chart types...');
|
||||
this.chartTypeService.getAllChartTypes().subscribe({
|
||||
next: (data) => {
|
||||
console.log('Chart types loaded:', data);
|
||||
this.chartTypes = data;
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart types:', error);
|
||||
this.showError('Error loading chart types: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createChartType(): void {
|
||||
if (!this.newChartType.name) {
|
||||
this.showError('Chart type name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
this.chartTypeService.createChartType(this.newChartType).subscribe({
|
||||
next: (data) => {
|
||||
this.chartTypes.push(data);
|
||||
this.newChartType = {};
|
||||
this.showAddChartTypeForm = false;
|
||||
this.showSuccess('Chart type created successfully');
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating chart type:', error);
|
||||
this.showError('Error creating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateChartType(): void {
|
||||
if (!this.selectedChartType || !this.selectedChartType.name) {
|
||||
this.showError('Chart type name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
this.chartTypeService.updateChartType(this.selectedChartType.id, this.selectedChartType).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.chartTypes.findIndex(ct => ct.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.chartTypes[index] = data;
|
||||
}
|
||||
this.selectedChartType = null;
|
||||
this.showSuccess('Chart type updated successfully');
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating chart type:', error);
|
||||
this.showError('Error updating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteChartType(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this chart type? This will also delete all related UI components, templates, and fields.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
this.chartTypeService.deleteChartType(id).subscribe({
|
||||
next: () => {
|
||||
this.chartTypes = this.chartTypes.filter(ct => ct.id !== id);
|
||||
// Clear related data if the deleted chart type was selected
|
||||
if (this.selectedChartType && this.selectedChartType.id === id) {
|
||||
this.selectedChartType = null;
|
||||
this.uiComponents = [];
|
||||
this.chartTemplates = [];
|
||||
this.dynamicFields = [];
|
||||
}
|
||||
this.showSuccess('Chart type deleted successfully');
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting chart type:', error);
|
||||
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectChartTypeForEdit(chartType: ChartType): void {
|
||||
this.selectedChartType = { ...chartType };
|
||||
}
|
||||
|
||||
// UI Component Methods
|
||||
loadUiComponents(chartTypeId: number): void {
|
||||
if (!chartTypeId) {
|
||||
this.uiComponents = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||
this.uiComponentService.getUiComponentsByChartType(chartTypeId).subscribe({
|
||||
next: (data) => {
|
||||
this.uiComponents = data;
|
||||
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading UI components:', error);
|
||||
this.showError('Error loading UI components: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createUiComponent(): void {
|
||||
if (!this.selectedChartType) {
|
||||
this.showError('Please select a chart type first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newUiComponent.componentName) {
|
||||
this.showError('Component name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||
|
||||
// Create a complete chartType object with only the ID
|
||||
const chartTypeWithId = {
|
||||
id: this.selectedChartType.id,
|
||||
name: '',
|
||||
displayName: '',
|
||||
description: '',
|
||||
isActive: true,
|
||||
createdAt: '',
|
||||
updatedAt: ''
|
||||
};
|
||||
|
||||
const uiComponentData = {
|
||||
...this.newUiComponent,
|
||||
chartType: chartTypeWithId
|
||||
};
|
||||
|
||||
this.uiComponentService.createUiComponent(uiComponentData).subscribe({
|
||||
next: (data) => {
|
||||
this.uiComponents.push(data);
|
||||
this.newUiComponent = {};
|
||||
this.showAddUiComponentForm = false;
|
||||
this.showSuccess('UI component created successfully');
|
||||
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating UI component:', error);
|
||||
this.showError('Error creating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateUiComponent(): void {
|
||||
if (!this.selectedUiComponent || !this.selectedUiComponent.componentName) {
|
||||
this.showError('Component name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||
this.uiComponentService.updateUiComponent(this.selectedUiComponent.id, this.selectedUiComponent).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.uiComponents.findIndex(uc => uc.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.uiComponents[index] = data;
|
||||
}
|
||||
this.selectedUiComponent = null;
|
||||
this.showSuccess('UI component updated successfully');
|
||||
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating UI component:', error);
|
||||
this.showError('Error updating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteUiComponent(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this UI component?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||
this.uiComponentService.deleteUiComponent(id).subscribe({
|
||||
next: () => {
|
||||
this.uiComponents = this.uiComponents.filter(uc => uc.id !== id);
|
||||
this.showSuccess('UI component deleted successfully');
|
||||
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting UI component:', error);
|
||||
this.showError('Error deleting UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectUiComponentForEdit(uiComponent: UiComponent): void {
|
||||
this.selectedUiComponent = { ...uiComponent };
|
||||
}
|
||||
|
||||
// Component Property Methods
|
||||
loadComponentProperties(componentId: number): void {
|
||||
if (!componentId) {
|
||||
this.componentProperties = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
this.componentPropertyService.getComponentPropertiesByComponent(componentId).subscribe({
|
||||
next: (data) => {
|
||||
this.componentProperties = data;
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading component properties:', error);
|
||||
this.showError('Error loading component properties: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createComponentProperty(): void {
|
||||
if (!this.selectedUiComponent) {
|
||||
this.showError('Please select a UI component first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newComponentProperty.propertyName) {
|
||||
this.showError('Property name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
|
||||
// Create a complete component object with only the ID
|
||||
const componentWithId = {
|
||||
id: this.selectedUiComponent.id
|
||||
} as UiComponent;
|
||||
|
||||
const componentPropertyData = {
|
||||
...this.newComponentProperty,
|
||||
component: componentWithId
|
||||
};
|
||||
|
||||
this.componentPropertyService.createComponentProperty(componentPropertyData).subscribe({
|
||||
next: (data) => {
|
||||
this.componentProperties.push(data);
|
||||
this.newComponentProperty = {};
|
||||
this.showAddComponentPropertyForm = false;
|
||||
this.showSuccess('Component property created successfully');
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating component property:', error);
|
||||
this.showError('Error creating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateComponentProperty(): void {
|
||||
if (!this.selectedComponentProperty || !this.selectedComponentProperty.propertyName) {
|
||||
this.showError('Property name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
this.componentPropertyService.updateComponentProperty(this.selectedComponentProperty.id, this.selectedComponentProperty).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.componentProperties.findIndex(cp => cp.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.componentProperties[index] = data;
|
||||
}
|
||||
this.selectedComponentProperty = null;
|
||||
this.showSuccess('Component property updated successfully');
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating component property:', error);
|
||||
this.showError('Error updating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteComponentProperty(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this component property?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
this.componentPropertyService.deleteComponentProperty(id).subscribe({
|
||||
next: () => {
|
||||
this.componentProperties = this.componentProperties.filter(cp => cp.id !== id);
|
||||
this.showSuccess('Component property deleted successfully');
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting component property:', error);
|
||||
this.showError('Error deleting component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectComponentPropertyForEdit(componentProperty: ComponentProperty): void {
|
||||
this.selectedComponentProperty = { ...componentProperty };
|
||||
}
|
||||
|
||||
// Chart Template Methods
|
||||
loadChartTemplates(chartTypeId: number): void {
|
||||
if (!chartTypeId) {
|
||||
this.chartTemplates = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||
this.chartTemplateService.getChartTemplatesByChartType(chartTypeId).subscribe({
|
||||
next: (data) => {
|
||||
this.chartTemplates = data;
|
||||
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart templates:', error);
|
||||
this.showError('Error loading chart templates: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createChartTemplate(): void {
|
||||
if (!this.selectedChartType) {
|
||||
this.showError('Please select a chart type first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newChartTemplate.templateName) {
|
||||
this.showError('Template name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||
|
||||
// Remove the chartType from the template data since we're passing it as a parameter
|
||||
const chartTemplateData = { ...this.newChartTemplate };
|
||||
// Remove chartType property if it exists
|
||||
delete (chartTemplateData as any).chartType;
|
||||
|
||||
this.chartTemplateService.createChartTemplate(chartTemplateData, this.selectedChartType.id).subscribe({
|
||||
next: (data) => {
|
||||
this.chartTemplates.push(data);
|
||||
this.newChartTemplate = {};
|
||||
this.showAddChartTemplateForm = false;
|
||||
this.showSuccess('Chart template created successfully');
|
||||
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating chart template:', error);
|
||||
this.showError('Error creating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateChartTemplate(): void {
|
||||
if (!this.selectedChartTemplate || !this.selectedChartTemplate.templateName) {
|
||||
this.showError('Template name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||
this.chartTemplateService.updateChartTemplate(this.selectedChartTemplate.id, this.selectedChartTemplate).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.chartTemplates.findIndex(ct => ct.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.chartTemplates[index] = data;
|
||||
}
|
||||
this.selectedChartTemplate = null;
|
||||
this.showSuccess('Chart template updated successfully');
|
||||
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating chart template:', error);
|
||||
this.showError('Error updating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteChartTemplate(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this chart template?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||
this.chartTemplateService.deleteChartTemplate(id).subscribe({
|
||||
next: () => {
|
||||
this.chartTemplates = this.chartTemplates.filter(ct => ct.id !== id);
|
||||
this.showSuccess('Chart template deleted successfully');
|
||||
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting chart template:', error);
|
||||
this.showError('Error deleting chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectChartTemplateForEdit(chartTemplate: ChartTemplate): void {
|
||||
this.selectedChartTemplate = { ...chartTemplate };
|
||||
}
|
||||
|
||||
// Dynamic Field Methods
|
||||
loadDynamicFields(chartTypeId: number): void {
|
||||
if (!chartTypeId) {
|
||||
this.dynamicFields = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||
this.dynamicFieldService.getDynamicFieldsByChartType(chartTypeId).subscribe({
|
||||
next: (data) => {
|
||||
this.dynamicFields = data;
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading dynamic fields:', error);
|
||||
this.showError('Error loading dynamic fields: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createDynamicField(): void {
|
||||
if (!this.selectedChartType) {
|
||||
this.showError('Please select a chart type first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newDynamicField.fieldName) {
|
||||
this.showError('Field name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||
|
||||
// Remove the chartType from the dynamic field data since we're passing it as a parameter
|
||||
const dynamicFieldData = { ...this.newDynamicField };
|
||||
// Remove chartType property if it exists
|
||||
delete (dynamicFieldData as any).chartType;
|
||||
|
||||
this.dynamicFieldService.createDynamicField(dynamicFieldData, this.selectedChartType.id).subscribe({
|
||||
next: (data) => {
|
||||
this.dynamicFields.push(data);
|
||||
this.newDynamicField = {};
|
||||
this.showAddDynamicFieldForm = false;
|
||||
this.showSuccess('Dynamic field created successfully');
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating dynamic field:', error);
|
||||
this.showError('Error creating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDynamicField(): void {
|
||||
if (!this.selectedDynamicField || !this.selectedDynamicField.fieldName) {
|
||||
this.showError('Field name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||
this.dynamicFieldService.updateDynamicField(this.selectedDynamicField.id, this.selectedDynamicField).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.dynamicFields.findIndex(df => df.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.dynamicFields[index] = data;
|
||||
}
|
||||
this.selectedDynamicField = null;
|
||||
this.showSuccess('Dynamic field updated successfully');
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating dynamic field:', error);
|
||||
this.showError('Error updating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteDynamicField(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this dynamic field?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||
this.dynamicFieldService.deleteDynamicField(id).subscribe({
|
||||
next: () => {
|
||||
this.dynamicFields = this.dynamicFields.filter(df => df.id !== id);
|
||||
this.showSuccess('Dynamic field deleted successfully');
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting dynamic field:', error);
|
||||
this.showError('Error deleting dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectDynamicFieldForEdit(dynamicField: DynamicField): void {
|
||||
this.selectedDynamicField = { ...dynamicField };
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
onChartTypeSelect(chartType: ChartType): void {
|
||||
this.selectedChartType = chartType;
|
||||
this.loadUiComponents(chartType.id);
|
||||
this.loadChartTemplates(chartType.id);
|
||||
this.loadDynamicFields(chartType.id);
|
||||
}
|
||||
|
||||
onUiComponentSelect(uiComponent: UiComponent): void {
|
||||
this.selectedUiComponent = uiComponent;
|
||||
this.loadComponentProperties(uiComponent.id);
|
||||
// Scroll to the Component Properties tab
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector('clr-tab[ng-reflect-ng-if="true"] button[clr-tab-link]:nth-child(3)');
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
resetForms(): void {
|
||||
this.newChartType = {};
|
||||
this.newUiComponent = {};
|
||||
this.newComponentProperty = {};
|
||||
this.newChartTemplate = {};
|
||||
this.newDynamicField = {};
|
||||
this.showAddChartTypeForm = false;
|
||||
this.showAddUiComponentForm = false;
|
||||
this.showAddComponentPropertyForm = false;
|
||||
this.showAddChartTemplateForm = false;
|
||||
this.showAddDynamicFieldForm = false;
|
||||
this.selectedChartType = null;
|
||||
this.selectedUiComponent = null;
|
||||
this.selectedComponentProperty = null;
|
||||
this.selectedChartTemplate = null;
|
||||
this.selectedDynamicField = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||
import { ChartTemplate } from './chart-config-manager.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ChartTemplateService {
|
||||
private chartTemplatesUrl = 'api/chart-templates';
|
||||
|
||||
constructor(private apiRequest: ApiRequestService) { }
|
||||
|
||||
// Get all chart templates for a chart type
|
||||
getChartTemplatesByChartType(chartTypeId: number): Observable<ChartTemplate[]> {
|
||||
const url = `${this.chartTemplatesUrl}/chart-type/${chartTypeId}`;
|
||||
return this.apiRequest.get(url);
|
||||
}
|
||||
|
||||
// Get chart template by ID
|
||||
getChartTemplateById(id: number): Observable<ChartTemplate> {
|
||||
const url = `${this.chartTemplatesUrl}/${id}`;
|
||||
return this.apiRequest.get(url);
|
||||
}
|
||||
|
||||
// Create new chart template with optional chart type ID as parameter
|
||||
createChartTemplate(chartTemplate: Partial<ChartTemplate>, chartTypeId?: number): Observable<ChartTemplate> {
|
||||
let url = this.chartTemplatesUrl;
|
||||
if (chartTypeId) {
|
||||
url = `${this.chartTemplatesUrl}?chartTypeId=${chartTypeId}`;
|
||||
}
|
||||
return this.apiRequest.post(url, chartTemplate);
|
||||
}
|
||||
|
||||
// Update chart template
|
||||
updateChartTemplate(id: number, chartTemplate: ChartTemplate): Observable<ChartTemplate> {
|
||||
const url = `${this.chartTemplatesUrl}/${id}`;
|
||||
return this.apiRequest.put(url, chartTemplate);
|
||||
}
|
||||
|
||||
// Delete chart template
|
||||
deleteChartTemplate(id: number): Observable<void> {
|
||||
const url = `${this.chartTemplatesUrl}/${id}`;
|
||||
return this.apiRequest.delete(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||
import { ComponentProperty } from './chart-config-manager.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ComponentPropertyService {
|
||||
private componentPropertiesUrl = 'api/component-properties';
|
||||
|
||||
constructor(private apiRequest: ApiRequestService) { }
|
||||
|
||||
// Get all component properties for a component
|
||||
getComponentPropertiesByComponent(componentId: number): Observable<ComponentProperty[]> {
|
||||
const url = `${this.componentPropertiesUrl}/component/${componentId}`;
|
||||
return this.apiRequest.get(url);
|
||||
}
|
||||
|
||||
// Get component property by ID
|
||||
getComponentPropertyById(id: number): Observable<ComponentProperty> {
|
||||
const url = `${this.componentPropertiesUrl}/${id}`;
|
||||
return this.apiRequest.get(url);
|
||||
}
|
||||
|
||||
// Create new component property
|
||||
createComponentProperty(componentProperty: Partial<ComponentProperty>): Observable<ComponentProperty> {
|
||||
return this.apiRequest.post(this.componentPropertiesUrl, componentProperty);
|
||||
}
|
||||
|
||||
// Update component property
|
||||
updateComponentProperty(id: number, componentProperty: ComponentProperty): Observable<ComponentProperty> {
|
||||
const url = `${this.componentPropertiesUrl}/${id}`;
|
||||
return this.apiRequest.put(url, componentProperty);
|
||||
}
|
||||
|
||||
// Delete component property
|
||||
deleteComponentProperty(id: number): Observable<void> {
|
||||
const url = `${this.componentPropertiesUrl}/${id}`;
|
||||
return this.apiRequest.delete(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, forkJoin } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||
import { ChartType, UiComponent, ComponentProperty, ChartTemplate, DynamicField } from './chart-config-manager.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DynamicChartLoaderService {
|
||||
private chartTypesUrl = 'api/chart-types';
|
||||
private uiComponentsUrl = 'api/ui-components';
|
||||
private componentPropertiesUrl = 'api/component-properties';
|
||||
private chartTemplatesUrl = 'api/chart-templates';
|
||||
private dynamicFieldsUrl = 'api/dynamic-fields';
|
||||
|
||||
constructor(private apiRequest: ApiRequestService) { }
|
||||
|
||||
/**
|
||||
* Load all chart configurations dynamically
|
||||
* This method fetches all chart types and their associated components, templates, and fields
|
||||
*/
|
||||
loadAllChartConfigurations(): Observable<any> {
|
||||
console.log('Loading all chart configurations dynamically');
|
||||
|
||||
// Load all chart types first
|
||||
return this.apiRequest.get(this.chartTypesUrl).pipe(
|
||||
map(chartTypes => {
|
||||
console.log('Loaded chart types:', chartTypes);
|
||||
return chartTypes;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load complete configuration for a specific chart type
|
||||
* This includes UI components, templates, and dynamic fields
|
||||
*/
|
||||
loadChartConfiguration(chartTypeId: number): Observable<{
|
||||
chartType: ChartType,
|
||||
uiComponents: UiComponent[],
|
||||
templates: ChartTemplate[],
|
||||
dynamicFields: DynamicField[]
|
||||
}> {
|
||||
console.log(`Loading complete configuration for chart type ${chartTypeId}`);
|
||||
|
||||
// Load all related data in parallel
|
||||
return forkJoin({
|
||||
chartType: this.apiRequest.get(`${this.chartTypesUrl}/${chartTypeId}`),
|
||||
uiComponents: this.apiRequest.get(`${this.uiComponentsUrl}/chart-type/${chartTypeId}`),
|
||||
templates: this.apiRequest.get(`${this.chartTemplatesUrl}/chart-type/${chartTypeId}`),
|
||||
dynamicFields: this.apiRequest.get(`${this.dynamicFieldsUrl}/chart-type/${chartTypeId}`)
|
||||
}).pipe(
|
||||
map(result => {
|
||||
console.log(`Loaded complete configuration for chart type ${chartTypeId}:`, result);
|
||||
return result;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load chart template for a specific chart type
|
||||
* This is used to render the chart UI dynamically
|
||||
*/
|
||||
loadChartTemplate(chartTypeId: number): Observable<ChartTemplate[]> {
|
||||
console.log(`Loading chart templates for chart type ${chartTypeId}`);
|
||||
return this.apiRequest.get(`${this.chartTemplatesUrl}/chart-type/${chartTypeId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load UI components for a specific chart type
|
||||
* These define what configuration fields are needed for the chart
|
||||
*/
|
||||
loadUiComponents(chartTypeId: number): Observable<UiComponent[]> {
|
||||
console.log(`Loading UI components for chart type ${chartTypeId}`);
|
||||
return this.apiRequest.get(`${this.uiComponentsUrl}/chart-type/${chartTypeId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load dynamic fields for a specific chart type
|
||||
* These define additional dynamic fields that can be used in the chart
|
||||
*/
|
||||
loadDynamicFields(chartTypeId: number): Observable<DynamicField[]> {
|
||||
console.log(`Loading dynamic fields for chart type ${chartTypeId}`);
|
||||
return this.apiRequest.get(`${this.dynamicFieldsUrl}/chart-type/${chartTypeId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chart type by name
|
||||
* This is useful for finding a chart type by its name rather than ID
|
||||
*/
|
||||
// getChartTypeByName(name: string): Observable<ChartType | null> {
|
||||
// console.log(`Finding chart type by name: ${name}`);
|
||||
// return this.apiRequest.get(`${this.chartTypesUrl}/byname?chartName=${name}`).pipe(
|
||||
// map((chartTypes: ChartType[]) => {
|
||||
// console.log('Available chart types:', chartTypes);
|
||||
// const chartType = chartTypes.find(ct => ct.name === name);
|
||||
// console.log(`Found chart type for name ${name}:`, chartType);
|
||||
// return chartType || null;
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
|
||||
getChartTypeByName(name: string): Observable<any> {
|
||||
console.log(`Finding chart type by name: ${name}`);
|
||||
return this.apiRequest.get(`${this.chartTypesUrl}/byname?chartName=${name}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Load all active chart types
|
||||
* This is used to populate the chart selection in the dashboard editor
|
||||
*/
|
||||
loadActiveChartTypes(): Observable<ChartType[]> {
|
||||
console.log('Loading active chart types');
|
||||
return this.apiRequest.get(`${this.chartTypesUrl}`).pipe(
|
||||
map((chartTypes: ChartType[]) => {
|
||||
const activeChartTypes = chartTypes.filter(ct => ct.isActive);
|
||||
console.log('Loaded active chart types:', activeChartTypes);
|
||||
return activeChartTypes;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||
import { DynamicField } from './chart-config-manager.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DynamicFieldService {
|
||||
private dynamicFieldsUrl = 'api/dynamic-fields';
|
||||
|
||||
constructor(private apiRequest: ApiRequestService) { }
|
||||
|
||||
// Get all dynamic fields for a chart type
|
||||
getDynamicFieldsByChartType(chartTypeId: number): Observable<DynamicField[]> {
|
||||
const url = `${this.dynamicFieldsUrl}/chart-type/${chartTypeId}`;
|
||||
return this.apiRequest.get(url);
|
||||
}
|
||||
|
||||
// Get dynamic field by ID
|
||||
getDynamicFieldById(id: number): Observable<DynamicField> {
|
||||
const url = `${this.dynamicFieldsUrl}/${id}`;
|
||||
return this.apiRequest.get(url);
|
||||
}
|
||||
|
||||
// Create new dynamic field with optional chart type ID as parameter
|
||||
createDynamicField(dynamicField: Partial<DynamicField>, chartTypeId?: number): Observable<DynamicField> {
|
||||
let url = this.dynamicFieldsUrl;
|
||||
if (chartTypeId) {
|
||||
url = `${this.dynamicFieldsUrl}?chartTypeId=${chartTypeId}`;
|
||||
}
|
||||
return this.apiRequest.post(url, dynamicField);
|
||||
}
|
||||
|
||||
// Update dynamic field
|
||||
updateDynamicField(id: number, dynamicField: DynamicField): Observable<DynamicField> {
|
||||
const url = `${this.dynamicFieldsUrl}/${id}`;
|
||||
return this.apiRequest.put(url, dynamicField);
|
||||
}
|
||||
|
||||
// Delete dynamic field
|
||||
deleteDynamicField(id: number): Observable<void> {
|
||||
const url = `${this.dynamicFieldsUrl}/${id}`;
|
||||
return this.apiRequest.delete(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<div class="chart-template-form">
|
||||
<form clrForm (ngSubmit)="onSubmit()" #chartTemplateForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Template Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="chartTemplate.templateName" name="templateName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>HTML Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="chartTemplate.templateHtml" name="templateHtml" rows="5" placeholder="<div class='chart-container'>...</div>"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>CSS Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="chartTemplate.templateCss" name="templateCss" rows="5" placeholder=".chart-container { ... }"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Default</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="chartTemplate.isDefault" name="isDefault" />
|
||||
<label>Default Template</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!chartTemplate.templateName">
|
||||
{{ isEdit ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
.chart-template-form {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { ChartTemplate } from '../chart-config-manager.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-template-form',
|
||||
templateUrl: './chart-template-form.component.html',
|
||||
styleUrls: ['./chart-template-form.component.scss']
|
||||
})
|
||||
export class ChartTemplateFormComponent {
|
||||
@Input() chartTemplate: Partial<ChartTemplate> = {};
|
||||
@Input() isEdit = false;
|
||||
@Output() save = new EventEmitter<Partial<ChartTemplate>>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
onSubmit(): void {
|
||||
this.save.emit(this.chartTemplate);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.cancel.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<div class="chart-type-form">
|
||||
<form clrForm (ngSubmit)="onSubmit()" #chartTypeForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="chartType.name" name="chartTypeName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="chartType.displayName" name="chartTypeDisplayName" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Description</label>
|
||||
<textarea clrTextarea [(ngModel)]="chartType.description" name="chartTypeDescription" rows="3"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Active</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="chartType.isActive" name="chartTypeIsActive" />
|
||||
<label>Active</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!chartType.name">
|
||||
{{ isEdit ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
.chart-type-form {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { ChartType } from '../chart-config-manager.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-type-form',
|
||||
templateUrl: './chart-type-form.component.html',
|
||||
styleUrls: ['./chart-type-form.component.scss']
|
||||
})
|
||||
export class ChartTypeFormComponent {
|
||||
@Input() chartType: Partial<ChartType> = {};
|
||||
@Input() isEdit = false;
|
||||
@Output() save = new EventEmitter<Partial<ChartType>>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
onSubmit(): void {
|
||||
this.save.emit(this.chartType);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.cancel.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="component-property-form">
|
||||
<form clrForm (ngSubmit)="onSubmit()" #componentPropertyForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Property Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="componentProperty.propertyName" name="propertyName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Property Value</label>
|
||||
<input clrInput type="text" [(ngModel)]="componentProperty.propertyValue" name="propertyValue" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-select-container>
|
||||
<label>Property Type</label>
|
||||
<select clrSelect [(ngModel)]="componentProperty.propertyType" name="propertyType">
|
||||
<option value="">Select a type</option>
|
||||
<option *ngFor="let type of propertyTypes" [value]="type">{{ type | titlecase }}</option>
|
||||
</select>
|
||||
</clr-select-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!componentProperty.propertyName">
|
||||
{{ isEdit ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
.component-property-form {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { ComponentProperty } from '../chart-config-manager.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-component-property-form',
|
||||
templateUrl: './component-property-form.component.html',
|
||||
styleUrls: ['./component-property-form.component.scss']
|
||||
})
|
||||
export class ComponentPropertyFormComponent {
|
||||
@Input() componentProperty: Partial<ComponentProperty> = {};
|
||||
@Input() isEdit = false;
|
||||
@Output() save = new EventEmitter<Partial<ComponentProperty>>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
propertyTypes = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'array',
|
||||
'object',
|
||||
'function'
|
||||
];
|
||||
|
||||
onSubmit(): void {
|
||||
this.save.emit(this.componentProperty);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.cancel.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<div class="dynamic-field-form">
|
||||
<form clrForm (ngSubmit)="onSubmit()" #dynamicFieldForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Field Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="dynamicField.fieldName" name="fieldName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="dynamicField.fieldLabel" name="fieldLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-select-container>
|
||||
<label>Field Type</label>
|
||||
<select clrSelect [(ngModel)]="dynamicField.fieldType" name="fieldType">
|
||||
<option value="">Select a type</option>
|
||||
<option *ngFor="let type of fieldTypes" [value]="type">{{ type | titlecase }}</option>
|
||||
</select>
|
||||
</clr-select-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Field Options (JSON format)</label>
|
||||
<textarea clrTextarea [(ngModel)]="dynamicField.fieldOptions" name="fieldOptions" rows="3" placeholder='{"option1": "Option 1", "option2": "Option 2"}'></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="dynamicField.sortOrder" name="fieldSortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="dynamicField.isRequired" name="fieldIsRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Show in UI</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="dynamicField.showInUi" name="fieldShowInUi" />
|
||||
<label>Show in UI</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!dynamicField.fieldName">
|
||||
{{ isEdit ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
.dynamic-field-form {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { DynamicField } from '../chart-config-manager.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dynamic-field-form',
|
||||
templateUrl: './dynamic-field-form.component.html',
|
||||
styleUrls: ['./dynamic-field-form.component.scss']
|
||||
})
|
||||
export class DynamicFieldFormComponent {
|
||||
@Input() dynamicField: Partial<DynamicField> = {};
|
||||
@Input() isEdit = false;
|
||||
@Output() save = new EventEmitter<Partial<DynamicField>>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
fieldTypes = [
|
||||
'text',
|
||||
'number',
|
||||
'email',
|
||||
'password',
|
||||
'date',
|
||||
'select',
|
||||
'checkbox',
|
||||
'radio',
|
||||
'textarea'
|
||||
];
|
||||
|
||||
onSubmit(): void {
|
||||
this.save.emit(this.dynamicField);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.cancel.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<div class="ui-component-form">
|
||||
<form clrForm (ngSubmit)="onSubmit()" #uiComponentForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Component Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="uiComponent.componentName" name="componentName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-select-container>
|
||||
<label>Component Type</label>
|
||||
<select clrSelect [(ngModel)]="uiComponent.componentType" name="componentType">
|
||||
<option value="">Select a type</option>
|
||||
<option *ngFor="let type of componentTypes" [value]="type">{{ type | titlecase }}</option>
|
||||
</select>
|
||||
</clr-select-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="uiComponent.displayLabel" name="displayLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Placeholder</label>
|
||||
<input clrInput type="text" [(ngModel)]="uiComponent.placeholder" name="placeholder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="uiComponent.sortOrder" name="sortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="uiComponent.isRequired" name="isRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!uiComponent.componentName">
|
||||
{{ isEdit ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
.ui-component-form {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { UiComponent } from '../chart-config-manager.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ui-component-form',
|
||||
templateUrl: './ui-component-form.component.html',
|
||||
styleUrls: ['./ui-component-form.component.scss']
|
||||
})
|
||||
export class UiComponentFormComponent {
|
||||
@Input() uiComponent: Partial<UiComponent> = {};
|
||||
@Input() isEdit = false;
|
||||
@Output() save = new EventEmitter<Partial<UiComponent>>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
componentTypes = [
|
||||
'input',
|
||||
'select',
|
||||
'checkbox',
|
||||
'radio',
|
||||
'textarea',
|
||||
'datepicker',
|
||||
'slider',
|
||||
'toggle'
|
||||
];
|
||||
|
||||
onSubmit(): void {
|
||||
this.save.emit(this.uiComponent);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.cancel.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||
import { UiComponent } from './chart-config-manager.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UiComponentService {
|
||||
private uiComponentsUrl = 'api/ui-components';
|
||||
|
||||
constructor(private apiRequest: ApiRequestService) { }
|
||||
|
||||
// Get all UI components for a chart type
|
||||
getUiComponentsByChartType(chartTypeId: number): Observable<UiComponent[]> {
|
||||
const url = `${this.uiComponentsUrl}/chart-type/${chartTypeId}`;
|
||||
console.log(`Fetching UI components for chart type ${chartTypeId} from ${url}`);
|
||||
return this.apiRequest.get(url);
|
||||
}
|
||||
|
||||
// Get UI component by ID
|
||||
getUiComponentById(id: number): Observable<UiComponent> {
|
||||
const url = `${this.uiComponentsUrl}/${id}`;
|
||||
console.log(`Fetching UI component ${id} from ${url}`);
|
||||
return this.apiRequest.get(url);
|
||||
}
|
||||
|
||||
// Create new UI component
|
||||
createUiComponent(uiComponent: Partial<UiComponent>): Observable<UiComponent> {
|
||||
console.log('Creating UI component:', uiComponent);
|
||||
return this.apiRequest.post(this.uiComponentsUrl, uiComponent);
|
||||
}
|
||||
|
||||
// Update UI component
|
||||
updateUiComponent(id: number, uiComponent: UiComponent): Observable<UiComponent> {
|
||||
const url = `${this.uiComponentsUrl}/${id}`;
|
||||
console.log(`Updating UI component ${id}:`, uiComponent);
|
||||
return this.apiRequest.put(url, uiComponent);
|
||||
}
|
||||
|
||||
// Delete UI component
|
||||
deleteUiComponent(id: number): Observable<void> {
|
||||
const url = `${this.uiComponentsUrl}/${id}`;
|
||||
console.log(`Deleting UI component ${id}`);
|
||||
return this.apiRequest.delete(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<div class="add-chart-type-page">
|
||||
<div class="header">
|
||||
<h2>Add New Chart Type</h2>
|
||||
</div>
|
||||
|
||||
<!-- Error and Success Messages -->
|
||||
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" *ngIf="successMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<form clrForm (ngSubmit)="createChartType()" #addChartTypeForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="chartType.name" name="chartTypeName" required />
|
||||
<clr-control-helper>Enter a unique name for the chart type (e.g., "line-chart", "bar-chart")</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="chartType.displayName" name="chartTypeDisplayName" />
|
||||
<clr-control-helper>This is the user-friendly name shown in the UI</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Description</label>
|
||||
<textarea clrTextarea [(ngModel)]="chartType.description" name="chartTypeDescription" rows="3"></textarea>
|
||||
<clr-control-helper>Provide a detailed description of this chart type and when to use it</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Active</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="chartType.isActive" name="chartTypeIsActive" />
|
||||
<label>Active</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<clr-control-helper>Deactivate chart types that should not be available for selection</clr-control-helper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!chartType.name || loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Create Chart Type
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h4>About Chart Types</h4>
|
||||
<p>Chart types define the different visualization options available in the dashboard builder. Each chart type can have:</p>
|
||||
<ul>
|
||||
<li>Associated UI components that define the configuration form</li>
|
||||
<li>Templates that define how the chart is rendered</li>
|
||||
<li>Dynamic fields that capture specific configuration parameters</li>
|
||||
</ul>
|
||||
<p>After creating a chart type, you can configure its components, templates, and fields in the chart configuration manager.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
.add-chart-type-page {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
h2 {
|
||||
color: #0079b8;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.card-block {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background-color: #f6f6f6;
|
||||
border-left: 4px solid #0079b8;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-chart-type',
|
||||
templateUrl: './add-chart-type.component.html',
|
||||
styleUrls: ['./add-chart-type.component.scss']
|
||||
})
|
||||
export class AddChartTypeComponent {
|
||||
chartType: Partial<ChartType> = {
|
||||
isActive: true
|
||||
};
|
||||
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
errorMessage: string | null = null;
|
||||
successMessage: string | null = null;
|
||||
|
||||
// Make ClrLoadingState available to template
|
||||
readonly ClrLoadingState = ClrLoadingState;
|
||||
|
||||
constructor(
|
||||
private chartTypeService: ChartTypeService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
// Show error message
|
||||
private showError(message: string): void {
|
||||
this.errorMessage = message;
|
||||
setTimeout(() => {
|
||||
this.errorMessage = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
private showSuccess(message: string): void {
|
||||
this.successMessage = message;
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
createChartType(): void {
|
||||
if (!this.chartType.name) {
|
||||
this.showError('Chart type name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.chartTypeService.createChartType(this.chartType).subscribe({
|
||||
next: (data) => {
|
||||
this.showSuccess('Chart type created successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
// Redirect to chart types list after a short delay
|
||||
setTimeout(() => {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||
}, 1500);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating chart type:', error);
|
||||
this.showError('Error creating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<div class="chart-type-fields-page">
|
||||
<div class="header">
|
||||
<h2>
|
||||
<button class="btn btn-link back-button" (click)="goBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
</button>
|
||||
Dynamic Fields for {{ chartType?.name || 'Chart Type' }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Error and Success Messages -->
|
||||
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" *ngIf="successMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Dynamic Field Form -->
|
||||
<div class="card" *ngIf="showAddForm">
|
||||
<div class="card-header">
|
||||
<h3>Add New Dynamic Field</h3>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<form clrForm (ngSubmit)="createDynamicField()" #addDynamicFieldForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Field Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldName" name="fieldName" required />
|
||||
<clr-control-helper>Enter a unique name for the dynamic field (e.g., "chart-title", "x-axis-label")</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldLabel" name="fieldLabel" />
|
||||
<clr-control-helper>User-friendly label shown in the configuration form</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldType" name="fieldType" />
|
||||
<clr-control-helper>Type of the field (e.g., "string", "number", "boolean")</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Field Options</label>
|
||||
<textarea clrTextarea [(ngModel)]="newDynamicField.fieldOptions" name="fieldOptions" rows="3"></textarea>
|
||||
<clr-control-helper>JSON options for the field (for select fields, etc.)</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="newDynamicField.sortOrder" name="fieldSortOrder" />
|
||||
<clr-control-helper>Order in which fields appear in the form</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.isRequired" name="fieldIsRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<clr-control-helper>Mark as required if this field must be filled</clr-control-helper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Show in UI</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.showInUi" name="fieldShowInUi" />
|
||||
<label>Visible</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<clr-control-helper>Uncheck to hide this field in the configuration form</clr-control-helper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!newDynamicField.fieldName || loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Create Dynamic Field
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="showAddForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Fields Grid -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col">
|
||||
<h3>Dynamic Fields</h3>
|
||||
</div>
|
||||
<div class="clr-col" style="text-align: right;">
|
||||
<button class="btn btn-primary" (click)="showAddForm = true">
|
||||
<cds-icon shape="plus"></cds-icon> Add Dynamic Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="loadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Field Name</clr-dg-column>
|
||||
<clr-dg-column>Field Label</clr-dg-column>
|
||||
<clr-dg-column>Field Type</clr-dg-column>
|
||||
<clr-dg-column>Required</clr-dg-column>
|
||||
<clr-dg-column>Visible</clr-dg-column>
|
||||
<clr-dg-column>Sort Order</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let dynamicField of dynamicFields" [clrDgItem]="dynamicField">
|
||||
<clr-dg-cell>{{dynamicField.fieldName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{dynamicField.fieldLabel}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{dynamicField.fieldType}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="dynamicField.isRequired" [class.label-danger]="!dynamicField.isRequired">
|
||||
{{dynamicField.isRequired ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="dynamicField.showInUi" [class.label-danger]="!dynamicField.showInUi">
|
||||
{{dynamicField.showInUi ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{dynamicField.sortOrder}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectDynamicFieldForEdit(dynamicField)" title="Edit">
|
||||
<cds-icon shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteDynamicField(dynamicField.id)" title="Delete">
|
||||
<cds-icon shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{dynamicFields.length}} dynamic field(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Dynamic Field Form -->
|
||||
<div class="card" *ngIf="selectedDynamicField">
|
||||
<div class="card-header">
|
||||
<h3>Edit Dynamic Field</h3>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<form clrForm (ngSubmit)="updateDynamicField()" #editDynamicFieldForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Field Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldName" name="editFieldName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldLabel" name="editFieldLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Field Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldType" name="editFieldType" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Field Options</label>
|
||||
<textarea clrTextarea [(ngModel)]="selectedDynamicField.fieldOptions" name="editFieldOptions" rows="3"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="selectedDynamicField.sortOrder" name="editFieldSortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.isRequired" name="editFieldIsRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Show in UI</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.showInUi" name="editFieldShowInUi" />
|
||||
<label>Visible</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!selectedDynamicField.fieldName || loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Update Dynamic Field
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="selectedDynamicField = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h4>About Dynamic Fields</h4>
|
||||
<p>Dynamic fields define the configurable parameters for a chart type. Each field represents:</p>
|
||||
<ul>
|
||||
<li>A configuration option that can be set when creating or editing a chart</li>
|
||||
<li>Metadata like type, label, and validation rules</li>
|
||||
<li>Visibility and requirement settings</li>
|
||||
</ul>
|
||||
<p>Dynamic fields allow you to create flexible chart configurations that can be customized per instance.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,128 @@
|
||||
.chart-type-fields-page {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
color: #0079b8;
|
||||
font-weight: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
padding: 0;
|
||||
cds-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.card-block {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
|
||||
&.label-success {
|
||||
background-color: #3d9970;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.label-danger {
|
||||
background-color: #d32f2f;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
margin-top: 10px;
|
||||
|
||||
clr-dg-cell {
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background-color: #f6f6f6;
|
||||
border-left: 4px solid #0079b8;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
.clr-row {
|
||||
flex-direction: column;
|
||||
|
||||
.clr-col {
|
||||
text-align: left !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||
import { DynamicField } from '../chart-config/chart-config-manager.component';
|
||||
import { DynamicFieldService } from '../chart-config/dynamic-field.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-type-fields',
|
||||
templateUrl: './chart-type-fields.component.html',
|
||||
styleUrls: ['./chart-type-fields.component.scss']
|
||||
})
|
||||
export class ChartTypeFieldsComponent implements OnInit {
|
||||
chartType: ChartType | null = null;
|
||||
dynamicFields: DynamicField[] = [];
|
||||
newDynamicField: Partial<DynamicField> = {
|
||||
isRequired: false,
|
||||
showInUi: true
|
||||
};
|
||||
selectedDynamicField: DynamicField | null = null;
|
||||
showAddForm = false;
|
||||
|
||||
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
errorMessage: string | null = null;
|
||||
successMessage: string | null = null;
|
||||
|
||||
// Make ClrLoadingState available to template
|
||||
readonly ClrLoadingState = ClrLoadingState;
|
||||
|
||||
constructor(
|
||||
private chartTypeService: ChartTypeService,
|
||||
private dynamicFieldService: DynamicFieldService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const chartTypeId = Number(this.route.snapshot.paramMap.get('id'));
|
||||
if (chartTypeId) {
|
||||
this.loadChartType(chartTypeId);
|
||||
this.loadDynamicFields(chartTypeId);
|
||||
} else {
|
||||
this.showError('Invalid chart type ID');
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
private showError(message: string): void {
|
||||
this.errorMessage = message;
|
||||
setTimeout(() => {
|
||||
this.errorMessage = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
private showSuccess(message: string): void {
|
||||
this.successMessage = message;
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
loadChartType(id: number): void {
|
||||
this.chartTypeService.getChartTypeById(id).subscribe({
|
||||
next: (data) => {
|
||||
this.chartType = data;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart type:', error);
|
||||
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadDynamicFields(chartTypeId: number): void {
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.dynamicFieldService.getDynamicFieldsByChartType(chartTypeId).subscribe({
|
||||
next: (data) => {
|
||||
this.dynamicFields = data;
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading dynamic fields:', error);
|
||||
this.showError('Error loading dynamic fields: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createDynamicField(): void {
|
||||
if (!this.chartType || !this.newDynamicField.fieldName) {
|
||||
this.showError('Field name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a copy without the chartType property
|
||||
const fieldData: Partial<DynamicField> = {
|
||||
fieldName: this.newDynamicField.fieldName,
|
||||
fieldLabel: this.newDynamicField.fieldLabel,
|
||||
fieldType: this.newDynamicField.fieldType,
|
||||
fieldOptions: this.newDynamicField.fieldOptions,
|
||||
isRequired: this.newDynamicField.isRequired,
|
||||
showInUi: this.newDynamicField.showInUi,
|
||||
sortOrder: this.newDynamicField.sortOrder
|
||||
};
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.dynamicFieldService.createDynamicField(fieldData, this.chartType.id).subscribe({
|
||||
next: (data) => {
|
||||
this.dynamicFields.push(data);
|
||||
this.newDynamicField = { isRequired: false, showInUi: true };
|
||||
this.showAddForm = false;
|
||||
this.showSuccess('Dynamic field created successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating dynamic field:', error);
|
||||
this.showError('Error creating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDynamicField(): void {
|
||||
if (!this.selectedDynamicField || !this.selectedDynamicField.fieldName) {
|
||||
this.showError('Field name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Updating dynamic field:', this.selectedDynamicField);
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.dynamicFieldService.updateDynamicField(this.selectedDynamicField.id, this.selectedDynamicField).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.dynamicFields.findIndex(df => df.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.dynamicFields[index] = data;
|
||||
}
|
||||
this.selectedDynamicField = null;
|
||||
this.showSuccess('Dynamic field updated successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating dynamic field:', error);
|
||||
this.showError('Error updating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteDynamicField(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this dynamic field?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.dynamicFieldService.deleteDynamicField(id).subscribe({
|
||||
next: () => {
|
||||
this.dynamicFields = this.dynamicFields.filter(df => df.id !== id);
|
||||
this.showSuccess('Dynamic field deleted successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting dynamic field:', error);
|
||||
this.showError('Error deleting dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectDynamicFieldForEdit(dynamicField: DynamicField): void {
|
||||
this.selectedDynamicField = { ...dynamicField };
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
if (this.chartType) {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types/edit', this.chartType.id]);
|
||||
} else {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<div class="chart-type-manager">
|
||||
<h2>Chart Type Management</h2>
|
||||
|
||||
<!-- Error and Success Messages -->
|
||||
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" *ngIf="successMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Types Grid -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col">
|
||||
<h3>Chart Types</h3>
|
||||
</div>
|
||||
<div class="clr-col" style="text-align: right;">
|
||||
<button class="btn btn-primary" [routerLink]="['/cns-portal/dashboardbuilder/chart-types/add']">
|
||||
<cds-icon shape="plus"></cds-icon> Add Chart Type
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Name</clr-dg-column>
|
||||
<clr-dg-column>Display Name</clr-dg-column>
|
||||
<clr-dg-column>Description</clr-dg-column>
|
||||
<clr-dg-column>Status</clr-dg-column>
|
||||
<clr-dg-column>Created At</clr-dg-column>
|
||||
<clr-dg-column>Updated At</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let chartType of chartTypes" [clrDgItem]="chartType">
|
||||
<clr-dg-cell>{{chartType.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.displayName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.description}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
|
||||
{{chartType.isActive ? 'Active' : 'Inactive'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.createdAt ? (chartType.createdAt | date:'short') : 'N/A'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartType.updatedAt ? (chartType.updatedAt | date:'short') : 'N/A'}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" [routerLink]="['/cns-portal/dashboardbuilder/chart-types/edit', chartType.id]" title="Edit">
|
||||
<cds-icon shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteChartType(chartType.id)" title="Delete">
|
||||
<cds-icon shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{chartTypes.length}} chart type(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
.chart-type-manager {
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
color: #0079b8;
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.card-header {
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
}
|
||||
|
||||
.card-block {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
|
||||
&.label-success {
|
||||
background-color: #3d9970;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.label-danger {
|
||||
background-color: #d32f2f;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
margin-top: 10px;
|
||||
|
||||
clr-dg-cell {
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
|
||||
.card-header {
|
||||
.clr-row {
|
||||
flex-direction: column;
|
||||
|
||||
.clr-col {
|
||||
text-align: left !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-type-manager',
|
||||
templateUrl: './chart-type-manager.component.html',
|
||||
styleUrls: ['./chart-type-manager.component.scss']
|
||||
})
|
||||
export class ChartTypeManagerComponent implements OnInit {
|
||||
chartTypes: ChartType[] = [];
|
||||
chartTypeLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
// Make ClrLoadingState available to template
|
||||
readonly ClrLoadingState = ClrLoadingState;
|
||||
|
||||
// Error handling
|
||||
errorMessage: string | null = null;
|
||||
successMessage: string | null = null;
|
||||
|
||||
constructor(private chartTypeService: ChartTypeService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadChartTypes();
|
||||
}
|
||||
|
||||
// Show error message
|
||||
private showError(message: string): void {
|
||||
this.errorMessage = message;
|
||||
setTimeout(() => {
|
||||
this.errorMessage = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
private showSuccess(message: string): void {
|
||||
this.successMessage = message;
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Chart Type Methods
|
||||
loadChartTypes(): void {
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
this.chartTypeService.getAllChartTypes().subscribe({
|
||||
next: (data) => {
|
||||
// Process the data to ensure dates are properly formatted
|
||||
this.chartTypes = data.map(chartType => ({
|
||||
...chartType,
|
||||
createdAt: this.formatDate(chartType.createdAt),
|
||||
updatedAt: this.formatDate(chartType.updatedAt)
|
||||
}));
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart types:', error);
|
||||
this.showError('Error loading chart types: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Format date to handle both string and object formats
|
||||
private formatDate(date: any): string {
|
||||
if (!date) return '';
|
||||
|
||||
// If it's already a string, return as is
|
||||
if (typeof date === 'string') {
|
||||
return date;
|
||||
}
|
||||
|
||||
// If it's an object, try to convert to string
|
||||
if (typeof date === 'object') {
|
||||
// Handle various date object formats
|
||||
if (date instanceof Date) {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
// Handle ISO string within object
|
||||
if (date.date) {
|
||||
return date.date;
|
||||
}
|
||||
|
||||
// Handle other object formats
|
||||
try {
|
||||
return new Date(date).toISOString();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
deleteChartType(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this chart type?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||
this.chartTypeService.deleteChartType(id).subscribe({
|
||||
next: () => {
|
||||
this.chartTypes = this.chartTypes.filter(ct => ct.id !== id);
|
||||
this.showSuccess('Chart type deleted successfully');
|
||||
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting chart type:', error);
|
||||
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-type-page',
|
||||
template: `
|
||||
<div class="chart-type-page">
|
||||
<app-chart-type-manager></app-chart-type-manager>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.chart-type-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chart-type-page {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ChartTypePageComponent { }
|
||||
@@ -0,0 +1,175 @@
|
||||
<div class="chart-type-templates-page">
|
||||
<div class="header">
|
||||
<h2>
|
||||
<button class="btn btn-link back-button" (click)="goBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
</button>
|
||||
Chart Templates for {{ chartType?.name || 'Chart Type' }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Error and Success Messages -->
|
||||
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" *ngIf="successMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Chart Template Form -->
|
||||
<div class="card" *ngIf="showAddForm">
|
||||
<div class="card-header">
|
||||
<h3>Add New Chart Template</h3>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<form clrForm (ngSubmit)="createChartTemplate()" #addChartTemplateForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Template Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="newChartTemplate.templateName" name="templateName" required />
|
||||
<clr-control-helper>Enter a unique name for the chart template</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>HTML Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateHtml" name="templateHtml" rows="5"></textarea>
|
||||
<clr-control-helper>HTML structure for rendering the chart</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>CSS Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateCss" name="templateCss" rows="5"></textarea>
|
||||
<clr-control-helper>CSS styling for the chart template</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Default</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newChartTemplate.isDefault" name="isDefault" />
|
||||
<label>Default Template</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<clr-control-helper>Mark as default template for this chart type</clr-control-helper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!newChartTemplate.templateName || loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Create Chart Template
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="showAddForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Templates Grid -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col">
|
||||
<h3>Chart Templates</h3>
|
||||
</div>
|
||||
<div class="clr-col" style="text-align: right;">
|
||||
<button class="btn btn-primary" (click)="showAddForm = true">
|
||||
<cds-icon shape="plus"></cds-icon> Add Chart Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="loadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Template Name</clr-dg-column>
|
||||
<clr-dg-column>Default</clr-dg-column>
|
||||
<clr-dg-column>Created At</clr-dg-column>
|
||||
<clr-dg-column>Updated At</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let chartTemplate of chartTemplates" [clrDgItem]="chartTemplate">
|
||||
<clr-dg-cell>{{chartTemplate.templateName}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="chartTemplate.isDefault" [class.label-danger]="!chartTemplate.isDefault">
|
||||
{{chartTemplate.isDefault ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartTemplate.createdAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{chartTemplate.updatedAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectChartTemplateForEdit(chartTemplate)" title="Edit">
|
||||
<cds-icon shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteChartTemplate(chartTemplate.id)" title="Delete">
|
||||
<cds-icon shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{chartTemplates.length}} chart template(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Chart Template Form -->
|
||||
<div class="card" *ngIf="selectedChartTemplate">
|
||||
<div class="card-header">
|
||||
<h3>Edit Chart Template</h3>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<form clrForm (ngSubmit)="updateChartTemplate()" #editChartTemplateForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Template Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedChartTemplate.templateName" name="editTemplateName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>HTML Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateHtml" name="editTemplateHtml" rows="5"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>CSS Template</label>
|
||||
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateCss" name="editTemplateCss" rows="5"></textarea>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Default</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedChartTemplate.isDefault" name="editIsDefault" />
|
||||
<label>Default Template</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!selectedChartTemplate.templateName || loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Update Chart Template
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="selectedChartTemplate = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h4>About Chart Templates</h4>
|
||||
<p>Chart templates define how a chart of this type is rendered. Each template includes:</p>
|
||||
<ul>
|
||||
<li>HTML structure that defines the chart layout</li>
|
||||
<li>CSS styling that controls the appearance</li>
|
||||
<li>A default flag to indicate the primary template</li>
|
||||
</ul>
|
||||
<p>Templates allow you to have multiple visual representations for the same chart type.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,128 @@
|
||||
.chart-type-templates-page {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
color: #0079b8;
|
||||
font-weight: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
padding: 0;
|
||||
cds-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.card-block {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
|
||||
&.label-success {
|
||||
background-color: #3d9970;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.label-danger {
|
||||
background-color: #d32f2f;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
margin-top: 10px;
|
||||
|
||||
clr-dg-cell {
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background-color: #f6f6f6;
|
||||
border-left: 4px solid #0079b8;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
.clr-row {
|
||||
flex-direction: column;
|
||||
|
||||
.clr-col {
|
||||
text-align: left !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||
import { ChartTemplateService } from '../chart-config/chart-template.service';
|
||||
import { ChartTemplate } from '../chart-config/chart-config-manager.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-type-templates',
|
||||
templateUrl: './chart-type-templates.component.html',
|
||||
styleUrls: ['./chart-type-templates.component.scss']
|
||||
})
|
||||
export class ChartTypeTemplatesComponent implements OnInit {
|
||||
chartType: ChartType | null = null;
|
||||
chartTemplates: ChartTemplate[] = [];
|
||||
newChartTemplate: Partial<ChartTemplate> = {
|
||||
isDefault: false
|
||||
};
|
||||
selectedChartTemplate: ChartTemplate | null = null;
|
||||
showAddForm = false;
|
||||
|
||||
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
errorMessage: string | null = null;
|
||||
successMessage: string | null = null;
|
||||
|
||||
// Make ClrLoadingState available to template
|
||||
readonly ClrLoadingState = ClrLoadingState;
|
||||
|
||||
constructor(
|
||||
private chartTypeService: ChartTypeService,
|
||||
private chartTemplateService: ChartTemplateService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const chartTypeId = Number(this.route.snapshot.paramMap.get('id'));
|
||||
if (chartTypeId) {
|
||||
this.loadChartType(chartTypeId);
|
||||
this.loadChartTemplates(chartTypeId);
|
||||
} else {
|
||||
this.showError('Invalid chart type ID');
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
private showError(message: string): void {
|
||||
this.errorMessage = message;
|
||||
setTimeout(() => {
|
||||
this.errorMessage = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
private showSuccess(message: string): void {
|
||||
this.successMessage = message;
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
loadChartType(id: number): void {
|
||||
this.chartTypeService.getChartTypeById(id).subscribe({
|
||||
next: (data) => {
|
||||
this.chartType = data;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart type:', error);
|
||||
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadChartTemplates(chartTypeId: number): void {
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.chartTemplateService.getChartTemplatesByChartType(chartTypeId).subscribe({
|
||||
next: (data) => {
|
||||
this.chartTemplates = data;
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart templates:', error);
|
||||
this.showError('Error loading chart templates: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createChartTemplate(): void {
|
||||
if (!this.chartType || !this.newChartTemplate.templateName) {
|
||||
this.showError('Template name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a copy without the chartType property
|
||||
const templateData: Partial<ChartTemplate> = {
|
||||
templateName: this.newChartTemplate.templateName,
|
||||
templateHtml: this.newChartTemplate.templateHtml,
|
||||
templateCss: this.newChartTemplate.templateCss,
|
||||
isDefault: this.newChartTemplate.isDefault
|
||||
};
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.chartTemplateService.createChartTemplate(templateData, this.chartType.id).subscribe({
|
||||
next: (data) => {
|
||||
this.chartTemplates.push(data);
|
||||
this.newChartTemplate = { isDefault: false };
|
||||
this.showAddForm = false;
|
||||
this.showSuccess('Chart template created successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating chart template:', error);
|
||||
this.showError('Error creating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateChartTemplate(): void {
|
||||
if (!this.selectedChartTemplate || !this.selectedChartTemplate.templateName) {
|
||||
this.showError('Template name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.chartTemplateService.updateChartTemplate(this.selectedChartTemplate.id, this.selectedChartTemplate).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.chartTemplates.findIndex(ct => ct.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.chartTemplates[index] = data;
|
||||
}
|
||||
this.selectedChartTemplate = null;
|
||||
this.showSuccess('Chart template updated successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating chart template:', error);
|
||||
this.showError('Error updating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteChartTemplate(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this chart template?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.chartTemplateService.deleteChartTemplate(id).subscribe({
|
||||
next: () => {
|
||||
this.chartTemplates = this.chartTemplates.filter(ct => ct.id !== id);
|
||||
this.showSuccess('Chart template deleted successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting chart template:', error);
|
||||
this.showError('Error deleting chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectChartTemplateForEdit(chartTemplate: ChartTemplate): void {
|
||||
this.selectedChartTemplate = { ...chartTemplate };
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
if (this.chartType) {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types/edit', this.chartType.id]);
|
||||
} else {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<div class="chart-type-ui-components-page">
|
||||
<div class="header">
|
||||
<h2>
|
||||
<button class="btn btn-link back-button" (click)="goBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
</button>
|
||||
UI Components for {{ chartType?.name || 'Chart Type' }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Error and Success Messages -->
|
||||
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" *ngIf="successMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add UI Component Form -->
|
||||
<div class="card" *ngIf="showAddForm">
|
||||
<div class="card-header">
|
||||
<h3>Add New UI Component</h3>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<form clrForm (ngSubmit)="createUiComponent()" #addUiComponentForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Component Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="newUiComponent.componentName" name="componentName" required />
|
||||
<clr-control-helper>Enter a unique name for the UI component (e.g., "title-config", "axis-config")</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Component Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="newUiComponent.componentType" name="componentType" />
|
||||
<clr-control-helper>Type of the component (e.g., "input", "select", "checkbox")</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="newUiComponent.displayLabel" name="displayLabel" />
|
||||
<clr-control-helper>User-friendly label shown in the configuration form</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Placeholder</label>
|
||||
<input clrInput type="text" [(ngModel)]="newUiComponent.placeholder" name="placeholder" />
|
||||
<clr-control-helper>Placeholder text for input fields</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="newUiComponent.sortOrder" name="sortOrder" />
|
||||
<clr-control-helper>Order in which components appear in the form</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="newUiComponent.isRequired" name="isRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<clr-control-helper>Mark as required if this component must be filled</clr-control-helper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!newUiComponent.componentName || loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Create UI Component
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="showAddForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UI Components Grid -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col">
|
||||
<h3>UI Components</h3>
|
||||
</div>
|
||||
<div class="clr-col" style="text-align: right;">
|
||||
<button class="btn btn-primary" (click)="showAddForm = true">
|
||||
<cds-icon shape="plus"></cds-icon> Add UI Component
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="loadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Component Name</clr-dg-column>
|
||||
<clr-dg-column>Component Type</clr-dg-column>
|
||||
<clr-dg-column>Display Label</clr-dg-column>
|
||||
<clr-dg-column>Required</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let uiComponent of uiComponents" [clrDgItem]="uiComponent">
|
||||
<clr-dg-cell>{{uiComponent.componentName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{uiComponent.componentType}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{uiComponent.displayLabel}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span class="label" [class.label-success]="uiComponent.isRequired" [class.label-danger]="!uiComponent.isRequired">
|
||||
{{uiComponent.isRequired ? 'Yes' : 'No'}}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell class="action-cell">
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-sm btn-icon" (click)="selectUiComponentForEdit(uiComponent)" title="Edit" [disabled]="loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="loadingState !== ClrLoadingState.LOADING" shape="pencil" aria-label="Edit"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteUiComponent(uiComponent.id)" title="Delete" [disabled]="loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="loadingState !== ClrLoadingState.LOADING" shape="trash" aria-label="Delete"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon btn-primary" (click)="onViewComponentProperties(uiComponent)" title="View Properties" [disabled]="loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="loadingState !== ClrLoadingState.LOADING" shape="eye" aria-label="View Properties"></cds-icon>
|
||||
</button>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{uiComponents.length}} UI component(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit UI Component Form -->
|
||||
<div class="card" *ngIf="selectedUiComponent && !showAddComponentPropertyForm && !selectedComponentProperty">
|
||||
<div class="card-header">
|
||||
<h3>Edit UI Component</h3>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<form clrForm (ngSubmit)="updateUiComponent()" #editUiComponentForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Component Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentName" name="editComponentName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Component Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentType" name="editComponentType" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Label</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedUiComponent.displayLabel" name="editDisplayLabel" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Placeholder</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedUiComponent.placeholder" name="editPlaceholder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Sort Order</label>
|
||||
<input clrInput type="number" [(ngModel)]="selectedUiComponent.sortOrder" name="editSortOrder" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Required</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="selectedUiComponent.isRequired" name="editIsRequired" />
|
||||
<label>Required</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!selectedUiComponent.componentName || loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Update UI Component
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="selectedUiComponent = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Component Properties Section -->
|
||||
<div class="card" *ngIf="selectedUiComponent">
|
||||
<div class="card-header">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col">
|
||||
<h3>Properties for {{selectedUiComponent?.componentName}}</h3>
|
||||
</div>
|
||||
<div class="clr-col" style="text-align: right;">
|
||||
<button class="btn btn-primary" (click)="showAddComponentPropertyForm = true">
|
||||
<cds-icon shape="plus"></cds-icon> Add Property
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Component Property Form -->
|
||||
<div class="card-block" *ngIf="showAddComponentPropertyForm">
|
||||
<form clrForm (ngSubmit)="createComponentProperty()" #addComponentPropertyForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Property Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyName" name="propertyName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Property Value</label>
|
||||
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyValue" name="propertyValue" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Property Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyType" name="propertyType" />
|
||||
</clr-input-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!newComponentProperty.propertyName || componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Create Property
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="showAddComponentPropertyForm = false">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Component Properties Table -->
|
||||
<div class="card-block">
|
||||
<clr-datagrid [clrDgLoading]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-dg-column>Property Name</clr-dg-column>
|
||||
<clr-dg-column>Property Value</clr-dg-column>
|
||||
<clr-dg-column>Property Type</clr-dg-column>
|
||||
<clr-dg-column>Created At</clr-dg-column>
|
||||
<clr-dg-column>Updated At</clr-dg-column>
|
||||
<clr-dg-column>Actions</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let property of componentProperties" [clrDgItem]="property">
|
||||
<clr-dg-cell>{{property.propertyName}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.propertyValue}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.propertyType}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.createdAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{property.updatedAt | date:'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<button class="btn btn-sm btn-icon" (click)="selectComponentPropertyForEdit(property)" title="Edit" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-icon" (click)="deleteComponentProperty(property.id)" title="Delete" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
{{componentProperties.length}} propertie(s)
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<!-- Edit Component Property Form -->
|
||||
<div class="card-block" *ngIf="selectedComponentProperty">
|
||||
<h4>Edit Component Property</h4>
|
||||
<form clrForm (ngSubmit)="updateComponentProperty()" #editComponentPropertyForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Property Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyName" name="editPropertyName" required />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Property Value</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyValue" name="editPropertyValue" />
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Property Type</label>
|
||||
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyType" name="editPropertyType" />
|
||||
</clr-input-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!selectedComponentProperty.propertyName || componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Update Property
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="selectedComponentProperty = null">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h4>About UI Components</h4>
|
||||
<p>UI components define the configuration form elements for a chart type. Each component represents:</p>
|
||||
<ul>
|
||||
<li>A form field that appears when configuring a chart of this type</li>
|
||||
<li>Metadata like label, placeholder, and validation rules</li>
|
||||
<li>An order in which they appear in the configuration form</li>
|
||||
</ul>
|
||||
<p>After creating UI components, you can define their properties using the "View Properties" button.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,156 @@
|
||||
.chart-type-ui-components-page {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
color: #0079b8;
|
||||
font-weight: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
padding: 0;
|
||||
cds-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.card-block {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
|
||||
&.label-success {
|
||||
background-color: #3d9970;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.label-danger {
|
||||
background-color: #d32f2f;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
margin-top: 10px;
|
||||
|
||||
clr-dg-cell {
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background-color: #f6f6f6;
|
||||
border-left: 4px solid #0079b8;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Component Properties Section Styles
|
||||
.action-cell {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-buttons .btn-icon {
|
||||
min-width: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// Ensure icons are visible
|
||||
cds-icon {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
clr-spinner {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
.clr-row {
|
||||
flex-direction: column;
|
||||
|
||||
.clr-col {
|
||||
text-align: left !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||
import { UiComponentService } from '../chart-config/ui-component.service';
|
||||
import { ComponentPropertyService } from '../chart-config/component-property.service';
|
||||
import { UiComponent, ComponentProperty } from '../chart-config/chart-config-manager.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-type-ui-components',
|
||||
templateUrl: './chart-type-ui-components.component.html',
|
||||
styleUrls: ['./chart-type-ui-components.component.scss']
|
||||
})
|
||||
export class ChartTypeUiComponentsComponent implements OnInit {
|
||||
chartType: ChartType | null = null;
|
||||
uiComponents: UiComponent[] = [];
|
||||
newUiComponent: Partial<UiComponent> = {
|
||||
isRequired: false
|
||||
};
|
||||
selectedUiComponent: UiComponent | null = null;
|
||||
showAddForm = false;
|
||||
|
||||
// Component Properties
|
||||
componentProperties: ComponentProperty[] = [];
|
||||
selectedComponentProperty: ComponentProperty | null = null;
|
||||
newComponentProperty: Partial<ComponentProperty> = {};
|
||||
showAddComponentPropertyForm = false;
|
||||
componentPropertyLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
|
||||
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
errorMessage: string | null = null;
|
||||
successMessage: string | null = null;
|
||||
|
||||
// Make ClrLoadingState available to template
|
||||
readonly ClrLoadingState = ClrLoadingState;
|
||||
|
||||
constructor(
|
||||
private chartTypeService: ChartTypeService,
|
||||
private uiComponentService: UiComponentService,
|
||||
private componentPropertyService: ComponentPropertyService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const chartTypeId = Number(this.route.snapshot.paramMap.get('id'));
|
||||
if (chartTypeId) {
|
||||
this.loadChartType(chartTypeId);
|
||||
this.loadUiComponents(chartTypeId);
|
||||
} else {
|
||||
this.showError('Invalid chart type ID');
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
private showError(message: string): void {
|
||||
this.errorMessage = message;
|
||||
setTimeout(() => {
|
||||
this.errorMessage = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
private showSuccess(message: string): void {
|
||||
this.successMessage = message;
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
loadChartType(id: number): void {
|
||||
this.chartTypeService.getChartTypeById(id).subscribe({
|
||||
next: (data) => {
|
||||
this.chartType = data;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart type:', error);
|
||||
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadUiComponents(chartTypeId: number): void {
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.uiComponentService.getUiComponentsByChartType(chartTypeId).subscribe({
|
||||
next: (data) => {
|
||||
this.uiComponents = data;
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading UI components:', error);
|
||||
this.showError('Error loading UI components: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createUiComponent(): void {
|
||||
if (!this.chartType || !this.newUiComponent.componentName) {
|
||||
this.showError('Component name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
|
||||
// Create a complete chartType object with only the ID (following the pattern in chart-config-manager)
|
||||
const chartTypeWithId = {
|
||||
id: this.chartType.id,
|
||||
name: '',
|
||||
displayName: '',
|
||||
description: '',
|
||||
isActive: true,
|
||||
createdAt: '',
|
||||
updatedAt: ''
|
||||
};
|
||||
|
||||
const componentData = {
|
||||
...this.newUiComponent,
|
||||
chartType: chartTypeWithId
|
||||
};
|
||||
|
||||
this.uiComponentService.createUiComponent(componentData).subscribe({
|
||||
next: (data) => {
|
||||
this.uiComponents.push(data);
|
||||
this.newUiComponent = { isRequired: false };
|
||||
this.showAddForm = false;
|
||||
this.showSuccess('UI component created successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating UI component:', error);
|
||||
this.showError('Error creating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateUiComponent(): void {
|
||||
if (!this.selectedUiComponent || !this.selectedUiComponent.componentName) {
|
||||
this.showError('Component name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.uiComponentService.updateUiComponent(this.selectedUiComponent.id, this.selectedUiComponent).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.uiComponents.findIndex(uc => uc.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.uiComponents[index] = data;
|
||||
}
|
||||
this.selectedUiComponent = null;
|
||||
this.showSuccess('UI component updated successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating UI component:', error);
|
||||
this.showError('Error updating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteUiComponent(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this UI component?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.uiComponentService.deleteUiComponent(id).subscribe({
|
||||
next: () => {
|
||||
this.uiComponents = this.uiComponents.filter(uc => uc.id !== id);
|
||||
this.showSuccess('UI component deleted successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting UI component:', error);
|
||||
this.showError('Error deleting UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectUiComponentForEdit(uiComponent: UiComponent): void {
|
||||
this.selectedUiComponent = { ...uiComponent };
|
||||
}
|
||||
|
||||
// Component Property Methods
|
||||
loadComponentProperties(componentId: number): void {
|
||||
if (!componentId) {
|
||||
this.componentProperties = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
this.componentPropertyService.getComponentPropertiesByComponent(componentId).subscribe({
|
||||
next: (data) => {
|
||||
this.componentProperties = data;
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading component properties:', error);
|
||||
this.showError('Error loading component properties: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createComponentProperty(): void {
|
||||
if (!this.selectedUiComponent) {
|
||||
this.showError('Please select a UI component first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newComponentProperty.propertyName) {
|
||||
this.showError('Property name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
|
||||
// Create a complete component object with only the ID
|
||||
const componentWithId = {
|
||||
id: this.selectedUiComponent.id
|
||||
} as UiComponent;
|
||||
|
||||
const componentPropertyData = {
|
||||
...this.newComponentProperty,
|
||||
component: componentWithId
|
||||
};
|
||||
|
||||
this.componentPropertyService.createComponentProperty(componentPropertyData).subscribe({
|
||||
next: (data) => {
|
||||
this.componentProperties.push(data);
|
||||
this.newComponentProperty = {};
|
||||
this.showAddComponentPropertyForm = false;
|
||||
this.showSuccess('Component property created successfully');
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating component property:', error);
|
||||
this.showError('Error creating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateComponentProperty(): void {
|
||||
if (!this.selectedComponentProperty || !this.selectedComponentProperty.propertyName) {
|
||||
this.showError('Property name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
this.componentPropertyService.updateComponentProperty(this.selectedComponentProperty.id, this.selectedComponentProperty).subscribe({
|
||||
next: (data) => {
|
||||
const index = this.componentProperties.findIndex(cp => cp.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.componentProperties[index] = data;
|
||||
}
|
||||
this.selectedComponentProperty = null;
|
||||
this.showSuccess('Component property updated successfully');
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating component property:', error);
|
||||
this.showError('Error updating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteComponentProperty(id: number): void {
|
||||
if (!confirm('Are you sure you want to delete this component property?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||
this.componentPropertyService.deleteComponentProperty(id).subscribe({
|
||||
next: () => {
|
||||
this.componentProperties = this.componentProperties.filter(cp => cp.id !== id);
|
||||
this.showSuccess('Component property deleted successfully');
|
||||
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting component property:', error);
|
||||
this.showError('Error deleting component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectComponentPropertyForEdit(componentProperty: ComponentProperty): void {
|
||||
this.selectedComponentProperty = { ...componentProperty };
|
||||
}
|
||||
|
||||
// Helper method to view properties for a UI component
|
||||
onViewComponentProperties(uiComponent: UiComponent): void {
|
||||
this.selectedUiComponent = uiComponent;
|
||||
this.loadComponentProperties(uiComponent.id);
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
if (this.chartType) {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types/edit', this.chartType.id]);
|
||||
} else {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||
|
||||
export interface ChartType {
|
||||
id: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ChartTypeService {
|
||||
private chartTypesUrl = 'api/chart-types';
|
||||
|
||||
constructor(private apiRequest: ApiRequestService) { }
|
||||
|
||||
// Get all chart types
|
||||
getAllChartTypes(): Observable<ChartType[]> {
|
||||
console.log('Fetching all chart types from', this.chartTypesUrl);
|
||||
return this.apiRequest.get(this.chartTypesUrl);
|
||||
}
|
||||
|
||||
// Get chart type by ID
|
||||
getChartTypeById(id: number): Observable<ChartType> {
|
||||
const url = `${this.chartTypesUrl}/${id}`;
|
||||
console.log(`Fetching chart type ${id} from ${url}`);
|
||||
return this.apiRequest.get(url);
|
||||
}
|
||||
|
||||
// Create new chart type
|
||||
createChartType(chartType: Partial<ChartType>): Observable<ChartType> {
|
||||
console.log('Creating chart type:', chartType);
|
||||
return this.apiRequest.post(this.chartTypesUrl, chartType);
|
||||
}
|
||||
|
||||
// Update chart type
|
||||
updateChartType(id: number, chartType: ChartType): Observable<ChartType> {
|
||||
const url = `${this.chartTypesUrl}/${id}`;
|
||||
console.log(`Updating chart type ${id}:`, chartType);
|
||||
return this.apiRequest.put(url, chartType);
|
||||
}
|
||||
|
||||
// Delete chart type
|
||||
deleteChartType(id: number): Observable<void> {
|
||||
const url = `${this.chartTypesUrl}/${id}`;
|
||||
console.log(`Deleting chart type ${id}`);
|
||||
return this.apiRequest.delete(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<div class="edit-chart-type-page">
|
||||
<div class="header">
|
||||
<h2>Edit Chart Type</h2>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div *ngIf="loadingState === ClrLoadingState.LOADING" class="loading-spinner">
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
<span>Loading chart type...</span>
|
||||
</div>
|
||||
|
||||
<!-- Error and Success Messages -->
|
||||
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" *ngIf="successMessage">
|
||||
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||
<cds-icon shape="close"></cds-icon>
|
||||
</button>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">{{ successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Type Form -->
|
||||
<div class="card" *ngIf="chartType && loadingState !== ClrLoadingState.LOADING">
|
||||
<div class="card-header">
|
||||
<div class="chart-type-header">
|
||||
<h3>{{ chartType.name }}</h3>
|
||||
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
|
||||
{{ chartType.isActive ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<form clrForm (ngSubmit)="updateChartType()" #editChartTypeForm="ngForm">
|
||||
<clr-input-container>
|
||||
<label>Name <span class="required">*</span></label>
|
||||
<input clrInput type="text" [(ngModel)]="chartType.name" name="chartTypeName" required />
|
||||
<clr-control-helper>Enter a unique name for the chart type (e.g., "line-chart", "bar-chart")</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-input-container>
|
||||
<label>Display Name</label>
|
||||
<input clrInput type="text" [(ngModel)]="chartType.displayName" name="chartTypeDisplayName" />
|
||||
<clr-control-helper>This is the user-friendly name shown in the UI</clr-control-helper>
|
||||
</clr-input-container>
|
||||
|
||||
<clr-textarea-container>
|
||||
<label>Description</label>
|
||||
<textarea clrTextarea [(ngModel)]="chartType.description" name="chartTypeDescription" rows="3"></textarea>
|
||||
<clr-control-helper>Provide a detailed description of this chart type and when to use it</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<label>Is Active</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox [(ngModel)]="chartType.isActive" name="chartTypeIsActive" />
|
||||
<label>Active</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<clr-control-helper>Deactivate chart types that should not be available for selection</clr-control-helper>
|
||||
</clr-checkbox-container>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" [disabled]="!chartType.name || loadingState === ClrLoadingState.LOADING">
|
||||
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||
Update Chart Type
|
||||
</button>
|
||||
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||
<button class="btn btn-danger" type="button" (click)="onDelete()" style="margin-left: auto;">
|
||||
<cds-icon shape="trash"></cds-icon> Delete
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related Entities Management -->
|
||||
<div class="card" *ngIf="chartType && loadingState !== ClrLoadingState.LOADING">
|
||||
<div class="card-header">
|
||||
<h3>Related Configuration</h3>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<div class="related-entities-grid">
|
||||
<div class="entity-card">
|
||||
<div class="entity-header">
|
||||
<cds-icon shape="view-list" size="24"></cds-icon>
|
||||
<h4>UI Components</h4>
|
||||
</div>
|
||||
<p>Manage the UI components that define the configuration form for this chart type.</p>
|
||||
<button class="btn btn-sm btn-link" [routerLink]="['/cns-portal/dashboardbuilder/chart-types', chartType.id, 'ui-components']">
|
||||
Manage UI Components
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="entity-card">
|
||||
<div class="entity-header">
|
||||
<cds-icon shape="template" size="24"></cds-icon>
|
||||
<h4>Chart Templates</h4>
|
||||
</div>
|
||||
<p>Manage the templates that define how this chart type is rendered.</p>
|
||||
<button class="btn btn-sm btn-link" [routerLink]="['/cns-portal/dashboardbuilder/chart-types', chartType.id, 'templates']">
|
||||
Manage Templates
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="entity-card">
|
||||
<div class="entity-header">
|
||||
<cds-icon shape="form" size="24"></cds-icon>
|
||||
<h4>Dynamic Fields</h4>
|
||||
</div>
|
||||
<p>Manage the dynamic fields that capture specific configuration parameters.</p>
|
||||
<button class="btn btn-sm btn-link" [routerLink]="['/cns-portal/dashboardbuilder/chart-types', chartType.id, 'fields']">
|
||||
Manage Fields
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Type Details -->
|
||||
<div class="card" *ngIf="chartType && loadingState !== ClrLoadingState.LOADING">
|
||||
<div class="card-header">
|
||||
<h3>Chart Type Details</h3>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<label>ID:</label>
|
||||
<span>{{ chartType.id }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>Created At:</label>
|
||||
<span>{{ chartType.createdAt ? (chartType.createdAt | date:'medium') : 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>Updated At:</label>
|
||||
<span>{{ chartType.updatedAt ? (chartType.updatedAt | date:'medium') : 'N/A' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h4>About Chart Types</h4>
|
||||
<p>Chart types define the different visualization options available in the dashboard builder. Each chart type can have:</p>
|
||||
<ul>
|
||||
<li>Associated UI components that define the configuration form</li>
|
||||
<li>Templates that define how the chart is rendered</li>
|
||||
<li>Dynamic fields that capture specific configuration parameters</li>
|
||||
</ul>
|
||||
<p>After creating a chart type, you can configure its components, templates, and fields using the management links above.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,163 @@
|
||||
.edit-chart-type-page {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
h2 {
|
||||
color: #0079b8;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.card-block {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
.chart-type-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.related-entities-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
|
||||
.entity-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #f9f9f9;
|
||||
|
||||
.entity-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
cds-icon {
|
||||
color: #0079b8;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 8px;
|
||||
background-color: #f6f6f6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background-color: #f6f6f6;
|
||||
border-left: 4px solid #0079b8;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
color: #0079b8;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-type-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
|
||||
.label {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.related-entities-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-chart-type',
|
||||
templateUrl: './edit-chart-type.component.html',
|
||||
styleUrls: ['./edit-chart-type.component.scss']
|
||||
})
|
||||
export class EditChartTypeComponent implements OnInit {
|
||||
chartType: ChartType | null = null;
|
||||
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
errorMessage: string | null = null;
|
||||
successMessage: string | null = null;
|
||||
|
||||
// Make ClrLoadingState available to template
|
||||
readonly ClrLoadingState = ClrLoadingState;
|
||||
|
||||
constructor(
|
||||
private chartTypeService: ChartTypeService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const id = Number(this.route.snapshot.paramMap.get('id'));
|
||||
if (id) {
|
||||
this.loadChartType(id);
|
||||
} else {
|
||||
this.showError('Invalid chart type ID');
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
private showError(message: string): void {
|
||||
this.errorMessage = message;
|
||||
setTimeout(() => {
|
||||
this.errorMessage = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
private showSuccess(message: string): void {
|
||||
this.successMessage = message;
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
loadChartType(id: number): void {
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.chartTypeService.getChartTypeById(id).subscribe({
|
||||
next: (data) => {
|
||||
// Process the data to ensure dates are properly formatted
|
||||
this.chartType = {
|
||||
...data,
|
||||
createdAt: this.formatDate(data.createdAt),
|
||||
updatedAt: this.formatDate(data.updatedAt)
|
||||
};
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading chart type:', error);
|
||||
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Format date to handle both string and object formats
|
||||
private formatDate(date: any): string {
|
||||
if (!date) return '';
|
||||
|
||||
// If it's already a string, return as is
|
||||
if (typeof date === 'string') {
|
||||
return date;
|
||||
}
|
||||
|
||||
// If it's an object, try to convert to string
|
||||
if (typeof date === 'object') {
|
||||
// Handle various date object formats
|
||||
if (date instanceof Date) {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
// Handle ISO string within object
|
||||
if (date.date) {
|
||||
return date.date;
|
||||
}
|
||||
|
||||
// Handle other object formats
|
||||
try {
|
||||
return new Date(date).toISOString();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
updateChartType(): void {
|
||||
if (!this.chartType || !this.chartType.name) {
|
||||
this.showError('Chart type name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.chartTypeService.updateChartType(this.chartType.id, this.chartType).subscribe({
|
||||
next: (data) => {
|
||||
// Process the data to ensure dates are properly formatted
|
||||
this.chartType = {
|
||||
...data,
|
||||
createdAt: this.formatDate(data.createdAt),
|
||||
updatedAt: this.formatDate(data.updatedAt)
|
||||
};
|
||||
this.showSuccess('Chart type updated successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
// Redirect to chart types list after a short delay
|
||||
setTimeout(() => {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||
}, 1500);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating chart type:', error);
|
||||
this.showError('Error updating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||
}
|
||||
|
||||
onDelete(): void {
|
||||
if (!this.chartType) return;
|
||||
|
||||
if (confirm('Are you sure you want to delete this chart type? This action cannot be undone.')) {
|
||||
this.loadingState = ClrLoadingState.LOADING;
|
||||
this.chartTypeService.deleteChartType(this.chartType.id).subscribe({
|
||||
next: () => {
|
||||
this.showSuccess('Chart type deleted successfully');
|
||||
this.loadingState = ClrLoadingState.SUCCESS;
|
||||
// Redirect to chart types list after a short delay
|
||||
setTimeout(() => {
|
||||
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||
}, 1500);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting chart type:', error);
|
||||
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||
this.loadingState = ClrLoadingState.ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
# Common Filter System - Example Usage
|
||||
|
||||
## Dashboard Layout
|
||||
|
||||
```
|
||||
+-----------------------------------------------------+
|
||||
| Common Filter Widget (Draggable) |
|
||||
| |
|
||||
| [Category ▼] [Status ▼] [Date Range] [Active -toggle]|
|
||||
| [Save Preset] [Preset Selector] [Reset] |
|
||||
+-----------------------------------------------------+
|
||||
| +-----------+ +-----------+ +-----------+ |
|
||||
| | Bar Chart | | Line Chart| | Pie Chart | |
|
||||
| | | | | | | |
|
||||
| | | | | | | |
|
||||
| +-----------+ +-----------+ +-----------+ |
|
||||
| |
|
||||
| +-----------+ +-----------+ +-----------+ |
|
||||
| | Table | | Map | | KPI Cards | |
|
||||
| | | | | | | |
|
||||
| | | | | | | |
|
||||
| +-----------+ +-----------+ +-----------+ |
|
||||
+-----------------------------------------------------+
|
||||
```
|
||||
|
||||
## Filter Configuration Example
|
||||
|
||||
### 1. Creating Filter Definitions
|
||||
|
||||
```typescript
|
||||
// In CommonFilterComponent or dashboard initialization
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
id: 'category',
|
||||
field: 'product_category',
|
||||
label: 'Category',
|
||||
type: 'dropdown',
|
||||
options: ['Electronics', 'Clothing', 'Home & Garden', 'Books', 'Sports']
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
field: 'order_status',
|
||||
label: 'Status',
|
||||
type: 'multiselect',
|
||||
options: ['Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled']
|
||||
},
|
||||
{
|
||||
id: 'date_range',
|
||||
field: 'order_date',
|
||||
label: 'Order Date',
|
||||
type: 'date-range'
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
field: 'is_active',
|
||||
label: 'Active Orders',
|
||||
type: 'toggle'
|
||||
}
|
||||
];
|
||||
|
||||
// Set filters in the service
|
||||
filterService.setFilters(filters);
|
||||
```
|
||||
|
||||
### 2. Setting Initial Filter Values
|
||||
|
||||
```typescript
|
||||
// Set initial values
|
||||
filterService.updateFilterValue('category', 'Electronics');
|
||||
filterService.updateFilterValue('status', ['Processing', 'Shipped']);
|
||||
filterService.updateFilterValue('date_range', {
|
||||
start: '2023-01-01',
|
||||
end: '2023-12-31'
|
||||
});
|
||||
filterService.updateFilterValue('active', true);
|
||||
```
|
||||
|
||||
### 3. Chart Component Integration
|
||||
|
||||
```typescript
|
||||
// In bar-chart.component.ts
|
||||
export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.refreshData();
|
||||
})
|
||||
);
|
||||
|
||||
// Load initial data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Get current filter values
|
||||
const filterValues = this.filterService.getFilterValues();
|
||||
|
||||
// Build filter parameters for API
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterValues).length > 0) {
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(filterValues).forEach(key => {
|
||||
const value = filterValues[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
filterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Make API call with filters
|
||||
this.dashboardService.getChartData(
|
||||
this.table,
|
||||
'bar',
|
||||
this.xAxis,
|
||||
this.yAxis,
|
||||
this.connection,
|
||||
'',
|
||||
'',
|
||||
filterParams
|
||||
).subscribe(data => {
|
||||
// Handle response
|
||||
this.processChartData(data);
|
||||
});
|
||||
}
|
||||
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. API Endpoint Example
|
||||
|
||||
```javascript
|
||||
// Backend API endpoint example
|
||||
app.get('/chart/getdashjson/bar', (req, res) => {
|
||||
const {
|
||||
tableName,
|
||||
xAxis,
|
||||
yAxes,
|
||||
sureId,
|
||||
filters // JSON string of filters
|
||||
} = req.query;
|
||||
|
||||
// Parse filters
|
||||
let filterObj = {};
|
||||
if (filters) {
|
||||
try {
|
||||
filterObj = JSON.parse(filters);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse filters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Build database query with filters
|
||||
let query = `SELECT ${xAxis}, ${yAxes} FROM ${tableName}`;
|
||||
const whereConditions = [];
|
||||
|
||||
// Add filter conditions
|
||||
Object.keys(filterObj).forEach(field => {
|
||||
const value = filterObj[field];
|
||||
if (Array.isArray(value)) {
|
||||
// Handle multiselect (IN clause)
|
||||
const values = value.map(v => `'${v}'`).join(',');
|
||||
whereConditions.push(`${field} IN (${values})`);
|
||||
} else if (typeof value === 'object' && value.start && value.end) {
|
||||
// Handle date range
|
||||
whereConditions.push(`${field} BETWEEN '${value.start}' AND '${value.end}'`);
|
||||
} else if (typeof value === 'boolean') {
|
||||
// Handle boolean
|
||||
whereConditions.push(`${field} = ${value}`);
|
||||
} else {
|
||||
// Handle text and other values
|
||||
whereConditions.push(`${field} = '${value}'`);
|
||||
}
|
||||
});
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
query += ` WHERE ${whereConditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
// Execute query and return results
|
||||
database.query(query, (err, results) => {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
} else {
|
||||
res.json({
|
||||
chartLabels: results.map(row => row[xAxis]),
|
||||
chartData: [{
|
||||
data: results.map(row => row[yAxes]),
|
||||
label: yAxes
|
||||
}]
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Filter Presets Example
|
||||
|
||||
### Saving a Preset
|
||||
|
||||
```typescript
|
||||
// Save current filter state as a preset
|
||||
const presetName = 'Q4 2023 Sales';
|
||||
filterService.savePreset(presetName);
|
||||
|
||||
// Preset is now available in the presets list
|
||||
const presets = filterService.getPresets(); // ['Q4 2023 Sales', ...]
|
||||
```
|
||||
|
||||
### Loading a Preset
|
||||
|
||||
```typescript
|
||||
// Load a saved preset
|
||||
filterService.loadPreset('Q4 2023 Sales');
|
||||
|
||||
// All charts will automatically refresh with the preset filters
|
||||
```
|
||||
|
||||
## Cross-Filtering Example
|
||||
|
||||
```typescript
|
||||
// In a chart component, handle click events
|
||||
onBarClick(event: any): void {
|
||||
const clickedCategory = event.active[0]._model.label;
|
||||
|
||||
// Update the category filter
|
||||
this.filterService.updateFilterValue('category', clickedCategory);
|
||||
|
||||
// All other charts will automatically update
|
||||
}
|
||||
```
|
||||
|
||||
## URL Synchronization Example
|
||||
|
||||
```typescript
|
||||
// Update URL when filters change
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
Object.keys(filters).forEach(key => {
|
||||
const value = filters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (typeof value === 'object') {
|
||||
if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) {
|
||||
// Date range
|
||||
if (value.start) queryParams.append(`${key}_start`, value.start);
|
||||
if (value.end) queryParams.append(`${key}_end`, value.end);
|
||||
} else {
|
||||
// Other objects as JSON
|
||||
queryParams.append(key, JSON.stringify(value));
|
||||
}
|
||||
} else {
|
||||
// Simple values
|
||||
queryParams.append(key, value.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update browser URL
|
||||
const newUrl = `${window.location.pathname}?${queryParams.toString()}`;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
});
|
||||
|
||||
// Load filters from URL on page load
|
||||
ngOnInit(): void {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const filterState: any = {};
|
||||
|
||||
// Parse URL parameters back into filter state
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
if (key.endsWith('_start')) {
|
||||
const fieldName = key.replace('_start', '');
|
||||
filterState[fieldName] = filterState[fieldName] || {};
|
||||
filterState[fieldName].start = value;
|
||||
} else if (key.endsWith('_end')) {
|
||||
const fieldName = key.replace('_end', '');
|
||||
filterState[fieldName] = filterState[fieldName] || {};
|
||||
filterState[fieldName].end = value;
|
||||
} else {
|
||||
try {
|
||||
filterState[key] = JSON.parse(value);
|
||||
} catch (e) {
|
||||
filterState[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply URL filters
|
||||
Object.keys(filterState).forEach(key => {
|
||||
this.filterService.updateFilterValue(key, filterState[key]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant CommonFilterComponent
|
||||
participant FilterService
|
||||
participant ChartComponent
|
||||
participant API
|
||||
|
||||
User->>CommonFilterComponent: Configure filters
|
||||
CommonFilterComponent->>FilterService: Update filter state
|
||||
FilterService->>ChartComponent: Notify filter change
|
||||
ChartComponent->>FilterService: Request current filters
|
||||
FilterService-->>ChartComponent: Return filter values
|
||||
ChartComponent->>API: Fetch data with filters
|
||||
API-->>ChartComponent: Return filtered data
|
||||
ChartComponent->>User: Display updated chart
|
||||
```
|
||||
|
||||
This implementation provides a robust, scalable solution for managing common filters across multiple dashboard components with real-time updates and flexible configuration options.# Common Filter System - Example Usage
|
||||
|
||||
## Dashboard Layout
|
||||
|
||||
```
|
||||
+-----------------------------------------------------+
|
||||
| Common Filter Widget (Draggable) |
|
||||
| |
|
||||
| [Category ▼] [Status ▼] [Date Range] [Active -toggle]|
|
||||
| [Save Preset] [Preset Selector] [Reset] |
|
||||
+-----------------------------------------------------+
|
||||
| +-----------+ +-----------+ +-----------+ |
|
||||
| | Bar Chart | | Line Chart| | Pie Chart | |
|
||||
| | | | | | | |
|
||||
| | | | | | | |
|
||||
| +-----------+ +-----------+ +-----------+ |
|
||||
| |
|
||||
| +-----------+ +-----------+ +-----------+ |
|
||||
| | Table | | Map | | KPI Cards | |
|
||||
| | | | | | | |
|
||||
| | | | | | | |
|
||||
| +-----------+ +-----------+ +-----------+ |
|
||||
+-----------------------------------------------------+
|
||||
```
|
||||
|
||||
## Filter Configuration Example
|
||||
|
||||
### 1. Creating Filter Definitions
|
||||
|
||||
```typescript
|
||||
// In CommonFilterComponent or dashboard initialization
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
id: 'category',
|
||||
field: 'product_category',
|
||||
label: 'Category',
|
||||
type: 'dropdown',
|
||||
options: ['Electronics', 'Clothing', 'Home & Garden', 'Books', 'Sports']
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
field: 'order_status',
|
||||
label: 'Status',
|
||||
type: 'multiselect',
|
||||
options: ['Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled']
|
||||
},
|
||||
{
|
||||
id: 'date_range',
|
||||
field: 'order_date',
|
||||
label: 'Order Date',
|
||||
type: 'date-range'
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
field: 'is_active',
|
||||
label: 'Active Orders',
|
||||
type: 'toggle'
|
||||
}
|
||||
];
|
||||
|
||||
// Set filters in the service
|
||||
filterService.setFilters(filters);
|
||||
```
|
||||
|
||||
### 2. Setting Initial Filter Values
|
||||
|
||||
```typescript
|
||||
// Set initial values
|
||||
filterService.updateFilterValue('category', 'Electronics');
|
||||
filterService.updateFilterValue('status', ['Processing', 'Shipped']);
|
||||
filterService.updateFilterValue('date_range', {
|
||||
start: '2023-01-01',
|
||||
end: '2023-12-31'
|
||||
});
|
||||
filterService.updateFilterValue('active', true);
|
||||
```
|
||||
|
||||
### 3. Chart Component Integration
|
||||
|
||||
```typescript
|
||||
// In bar-chart.component.ts
|
||||
export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.refreshData();
|
||||
})
|
||||
);
|
||||
|
||||
// Load initial data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Get current filter values
|
||||
const filterValues = this.filterService.getFilterValues();
|
||||
|
||||
// Build filter parameters for API
|
||||
let filterParams = '';
|
||||
if (Object.keys(filterValues).length > 0) {
|
||||
const filterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (this.baseFilters && this.baseFilters.length > 0) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(filterValues).forEach(key => {
|
||||
const value = filterValues[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
filterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Make API call with filters
|
||||
this.dashboardService.getChartData(
|
||||
this.table,
|
||||
'bar',
|
||||
this.xAxis,
|
||||
this.yAxis,
|
||||
this.connection,
|
||||
'',
|
||||
'',
|
||||
filterParams
|
||||
).subscribe(data => {
|
||||
// Handle response
|
||||
this.processChartData(data);
|
||||
});
|
||||
}
|
||||
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. API Endpoint Example
|
||||
|
||||
```javascript
|
||||
// Backend API endpoint example
|
||||
app.get('/chart/getdashjson/bar', (req, res) => {
|
||||
const {
|
||||
tableName,
|
||||
xAxis,
|
||||
yAxes,
|
||||
sureId,
|
||||
filters // JSON string of filters
|
||||
} = req.query;
|
||||
|
||||
// Parse filters
|
||||
let filterObj = {};
|
||||
if (filters) {
|
||||
try {
|
||||
filterObj = JSON.parse(filters);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse filters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Build database query with filters
|
||||
let query = `SELECT ${xAxis}, ${yAxes} FROM ${tableName}`;
|
||||
const whereConditions = [];
|
||||
|
||||
// Add filter conditions
|
||||
Object.keys(filterObj).forEach(field => {
|
||||
const value = filterObj[field];
|
||||
if (Array.isArray(value)) {
|
||||
// Handle multiselect (IN clause)
|
||||
const values = value.map(v => `'${v}'`).join(',');
|
||||
whereConditions.push(`${field} IN (${values})`);
|
||||
} else if (typeof value === 'object' && value.start && value.end) {
|
||||
// Handle date range
|
||||
whereConditions.push(`${field} BETWEEN '${value.start}' AND '${value.end}'`);
|
||||
} else if (typeof value === 'boolean') {
|
||||
// Handle boolean
|
||||
whereConditions.push(`${field} = ${value}`);
|
||||
} else {
|
||||
// Handle text and other values
|
||||
whereConditions.push(`${field} = '${value}'`);
|
||||
}
|
||||
});
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
query += ` WHERE ${whereConditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
// Execute query and return results
|
||||
database.query(query, (err, results) => {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
} else {
|
||||
res.json({
|
||||
chartLabels: results.map(row => row[xAxis]),
|
||||
chartData: [{
|
||||
data: results.map(row => row[yAxes]),
|
||||
label: yAxes
|
||||
}]
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Filter Presets Example
|
||||
|
||||
### Saving a Preset
|
||||
|
||||
```typescript
|
||||
// Save current filter state as a preset
|
||||
const presetName = 'Q4 2023 Sales';
|
||||
filterService.savePreset(presetName);
|
||||
|
||||
// Preset is now available in the presets list
|
||||
const presets = filterService.getPresets(); // ['Q4 2023 Sales', ...]
|
||||
```
|
||||
|
||||
### Loading a Preset
|
||||
|
||||
```typescript
|
||||
// Load a saved preset
|
||||
filterService.loadPreset('Q4 2023 Sales');
|
||||
|
||||
// All charts will automatically refresh with the preset filters
|
||||
```
|
||||
|
||||
## Cross-Filtering Example
|
||||
|
||||
```typescript
|
||||
// In a chart component, handle click events
|
||||
onBarClick(event: any): void {
|
||||
const clickedCategory = event.active[0]._model.label;
|
||||
|
||||
// Update the category filter
|
||||
this.filterService.updateFilterValue('category', clickedCategory);
|
||||
|
||||
// All other charts will automatically update
|
||||
}
|
||||
```
|
||||
|
||||
## URL Synchronization Example
|
||||
|
||||
```typescript
|
||||
// Update URL when filters change
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
Object.keys(filters).forEach(key => {
|
||||
const value = filters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (typeof value === 'object') {
|
||||
if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) {
|
||||
// Date range
|
||||
if (value.start) queryParams.append(`${key}_start`, value.start);
|
||||
if (value.end) queryParams.append(`${key}_end`, value.end);
|
||||
} else {
|
||||
// Other objects as JSON
|
||||
queryParams.append(key, JSON.stringify(value));
|
||||
}
|
||||
} else {
|
||||
// Simple values
|
||||
queryParams.append(key, value.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update browser URL
|
||||
const newUrl = `${window.location.pathname}?${queryParams.toString()}`;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
});
|
||||
|
||||
// Load filters from URL on page load
|
||||
ngOnInit(): void {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const filterState: any = {};
|
||||
|
||||
// Parse URL parameters back into filter state
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
if (key.endsWith('_start')) {
|
||||
const fieldName = key.replace('_start', '');
|
||||
filterState[fieldName] = filterState[fieldName] || {};
|
||||
filterState[fieldName].start = value;
|
||||
} else if (key.endsWith('_end')) {
|
||||
const fieldName = key.replace('_end', '');
|
||||
filterState[fieldName] = filterState[fieldName] || {};
|
||||
filterState[fieldName].end = value;
|
||||
} else {
|
||||
try {
|
||||
filterState[key] = JSON.parse(value);
|
||||
} catch (e) {
|
||||
filterState[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply URL filters
|
||||
Object.keys(filterState).forEach(key => {
|
||||
this.filterService.updateFilterValue(key, filterState[key]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant CommonFilterComponent
|
||||
participant FilterService
|
||||
participant ChartComponent
|
||||
participant API
|
||||
|
||||
User->>CommonFilterComponent: Configure filters
|
||||
CommonFilterComponent->>FilterService: Update filter state
|
||||
FilterService->>ChartComponent: Notify filter change
|
||||
ChartComponent->>FilterService: Request current filters
|
||||
FilterService-->>ChartComponent: Return filter values
|
||||
ChartComponent->>API: Fetch data with filters
|
||||
API-->>ChartComponent: Return filtered data
|
||||
ChartComponent->>User: Display updated chart
|
||||
```
|
||||
|
||||
This implementation provides a robust, scalable solution for managing common filters across multiple dashboard components with real-time updates and flexible configuration options.
|
||||
@@ -0,0 +1,219 @@
|
||||
# Common Filter Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation provides a centralized filter system that allows users to control multiple charts simultaneously through a draggable filter widget. The system consists of:
|
||||
|
||||
1. **FilterService** - Central service managing filter definitions and state
|
||||
2. **CommonFilterComponent** - Draggable widget for configuring filters
|
||||
3. **Chart Components** - Updated chart components that subscribe to filter changes
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[FilterService] --> B[CommonFilterComponent]
|
||||
A --> C[BarChartComponent]
|
||||
A --> D[LineChartComponent]
|
||||
A --> E[OtherChartComponents]
|
||||
F[User Interaction] --> B
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. FilterService
|
||||
|
||||
The central service that manages:
|
||||
- Filter definitions (type, options, labels)
|
||||
- Current filter values
|
||||
- Filter presets
|
||||
- Query parameter generation for API calls
|
||||
|
||||
#### Methods:
|
||||
- `addFilter()` - Add a new filter
|
||||
- `removeFilter()` - Remove a filter
|
||||
- `updateFilterValue()` - Update a filter's value
|
||||
- `getFilterValues()` - Get current filter values
|
||||
- `resetFilters()` - Reset all filters to default values
|
||||
- `savePreset()` - Save current filter state as a preset
|
||||
- `loadPreset()` - Load a saved preset
|
||||
- `buildQueryParams()` - Generate query parameters for API calls
|
||||
|
||||
### 2. CommonFilterComponent
|
||||
|
||||
A draggable widget that provides the UI for:
|
||||
- Adding/removing filters
|
||||
- Configuring filter properties
|
||||
- Setting filter values
|
||||
- Managing presets
|
||||
|
||||
### 3. Chart Components
|
||||
|
||||
Updated chart components that:
|
||||
- Subscribe to filter changes
|
||||
- Automatically refresh when filters change
|
||||
- Include filter values in API calls
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Filter Types Supported
|
||||
|
||||
1. **Text** - Simple text input
|
||||
2. **Dropdown** - Single selection from options
|
||||
3. **Multiselect** - Multiple selection from options
|
||||
4. **Date Range** - Start and end date selection
|
||||
5. **Toggle** - Boolean on/off switch
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. User adds/configures filters in CommonFilterComponent
|
||||
2. FilterService stores filter definitions and values
|
||||
3. Chart components subscribe to FilterService.filterState$
|
||||
4. When filters change, charts automatically refresh
|
||||
5. Filter values are included in API calls as query parameters
|
||||
|
||||
### API Integration
|
||||
|
||||
Filters are passed to the backend as query parameters:
|
||||
- Text filter: `name=John`
|
||||
- Dropdown filter: `category=A`
|
||||
- Multiselect filter: `tags=["tag1","tag2"]`
|
||||
- Date range filter: `date_start=2023-01-01&date_end=2023-12-31`
|
||||
- Toggle filter: `isActive=true`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Adding the Common Filter Widget
|
||||
|
||||
1. Drag the "Common Filter" widget from the component palette
|
||||
2. Place it at the top of the dashboard
|
||||
3. Configure filters as needed
|
||||
|
||||
### Creating Filters
|
||||
|
||||
```typescript
|
||||
// Add a text filter
|
||||
const textFilter: Filter = {
|
||||
id: 'name',
|
||||
field: 'name',
|
||||
label: 'Name',
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
// Add a dropdown filter
|
||||
const dropdownFilter: Filter = {
|
||||
id: 'category',
|
||||
field: 'category',
|
||||
label: 'Category',
|
||||
type: 'dropdown',
|
||||
options: ['A', 'B', 'C']
|
||||
};
|
||||
```
|
||||
|
||||
### Consuming Filters in Charts
|
||||
|
||||
```typescript
|
||||
// In chart component
|
||||
constructor(private filterService: FilterService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Get current filter values
|
||||
const filterValues = this.filterService.getFilterValues();
|
||||
|
||||
// Include in API call
|
||||
const queryParams = this.filterService.buildQueryParams();
|
||||
// Use queryParams in API call
|
||||
}
|
||||
```
|
||||
|
||||
## JSON Structures
|
||||
|
||||
### Filter Definition
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "filter_123456",
|
||||
"field": "category",
|
||||
"label": "Category",
|
||||
"type": "dropdown",
|
||||
"options": ["Electronics", "Clothing", "Books"],
|
||||
"value": "Electronics"
|
||||
}
|
||||
```
|
||||
|
||||
### Filter State
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "Electronics",
|
||||
"price_range": {
|
||||
"start": 100,
|
||||
"end": 500
|
||||
},
|
||||
"in_stock": true,
|
||||
"tags": ["sale", "featured"]
|
||||
}
|
||||
```
|
||||
|
||||
### API Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"chartLabels": ["Jan", "Feb", "Mar"],
|
||||
"chartData": [
|
||||
{
|
||||
"data": [10, 20, 30],
|
||||
"label": "Sales"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Cross-Filtering Support
|
||||
|
||||
Charts can also support cross-filtering where clicking on one chart updates the common filters:
|
||||
|
||||
1. Implement click handlers in chart components
|
||||
2. Update filter values through FilterService
|
||||
3. All other charts automatically refresh
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
onChartClick(dataPoint: any): void {
|
||||
// Update a filter based on chart click
|
||||
this.filterService.updateFilterValue('category', dataPoint.category);
|
||||
}
|
||||
```
|
||||
|
||||
## Preset Management
|
||||
|
||||
Users can save and load filter presets:
|
||||
|
||||
1. Configure desired filters
|
||||
2. Enter a preset name and click "Save Preset"
|
||||
3. Select preset from dropdown to load
|
||||
4. Delete presets as needed
|
||||
|
||||
## URL Synchronization
|
||||
|
||||
Filter states can be synchronized with URL query parameters for shareable dashboards:
|
||||
|
||||
1. On filter change, update URL query parameters
|
||||
2. On page load, read filters from URL
|
||||
3. Apply filters to charts
|
||||
|
||||
## Optional Enhancements
|
||||
|
||||
1. **Real-time Updates** - WebSocket integration for live data
|
||||
2. **Advanced Filtering** - Custom filter expressions
|
||||
3. **Filter Dependencies** - Conditional filters based on other filter values
|
||||
4. **Analytics** - Track most used filters and combinations
|
||||
@@ -0,0 +1,68 @@
|
||||
.chart-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
|
||||
.chart-header {
|
||||
padding: 10px 15px;
|
||||
background: #f8f8f8;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
// Ensure chart containers fill available space
|
||||
::ng-deep canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.chart-wrapper {
|
||||
.chart-header {
|
||||
padding: 8px 12px;
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chart-wrapper {
|
||||
.chart-header {
|
||||
padding: 6px 10px;
|
||||
|
||||
h5 {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ChartWrapperComponent } from './chart-wrapper.component';
|
||||
import { FilterService } from './filter.service';
|
||||
|
||||
describe('ChartWrapperComponent', () => {
|
||||
let component: ChartWrapperComponent;
|
||||
let fixture: ComponentFixture<ChartWrapperComponent>;
|
||||
let filterService: FilterService;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ChartWrapperComponent],
|
||||
providers: [FilterService]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChartWrapperComponent);
|
||||
component = fixture.componentInstance;
|
||||
filterService = TestBed.inject(FilterService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should subscribe to filter changes on init', () => {
|
||||
spyOn(filterService.filterState$, 'subscribe');
|
||||
component.ngOnInit();
|
||||
expect(filterService.filterState$.subscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should unsubscribe from filter changes on destroy', () => {
|
||||
component.ngOnInit();
|
||||
spyOn(component['filterSubscription']!, 'unsubscribe');
|
||||
component.ngOnDestroy();
|
||||
expect(component['filterSubscription']!.unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Component, Input, OnInit, OnDestroy, ComponentRef, ViewChild, ViewContainerRef, HostListener } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FilterService } from './filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-wrapper',
|
||||
template: `
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-header" *ngIf="chartTitle">
|
||||
<h5>{{ chartTitle }}</h5>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<ng-container #chartContainer></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./chart-wrapper.component.scss']
|
||||
})
|
||||
export class ChartWrapperComponent implements OnInit, OnDestroy {
|
||||
@Input() chartComponent: any;
|
||||
@Input() chartInputs: any = {};
|
||||
@Input() chartTitle: string = '';
|
||||
|
||||
@ViewChild('chartContainer', { read: ViewContainerRef }) chartContainer!: ViewContainerRef;
|
||||
|
||||
private componentRef: ComponentRef<any> | null = null;
|
||||
private filterSubscription: Subscription | null = null;
|
||||
|
||||
constructor(private filterService: FilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadChartComponent();
|
||||
this.subscribeToFilters();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.filterSubscription) {
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
if (this.componentRef) {
|
||||
this.componentRef.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle window resize events
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event: any) {
|
||||
// Notify the chart component to resize if it has a resize method
|
||||
if (this.componentRef && this.componentRef.instance) {
|
||||
const chartInstance = this.componentRef.instance;
|
||||
|
||||
// If it's a chart component with an onResize method, call it
|
||||
if (chartInstance.onResize && typeof chartInstance.onResize === 'function') {
|
||||
chartInstance.onResize();
|
||||
}
|
||||
|
||||
// If it's a chart component with a chart property (from BaseChartDirective), resize it
|
||||
if (chartInstance.chart && typeof chartInstance.chart.resize === 'function') {
|
||||
setTimeout(() => {
|
||||
chartInstance.chart.resize();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadChartComponent(): void {
|
||||
if (this.chartContainer && this.chartComponent) {
|
||||
this.chartContainer.clear();
|
||||
const factory = this.chartContainer.createComponent(this.chartComponent);
|
||||
this.componentRef = factory;
|
||||
|
||||
// Set initial inputs
|
||||
Object.keys(this.chartInputs).forEach(key => {
|
||||
factory.instance[key] = this.chartInputs[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToFilters(): void {
|
||||
this.filterSubscription = this.filterService.filterState$.subscribe(filterValues => {
|
||||
this.updateChartWithFilters(filterValues);
|
||||
});
|
||||
}
|
||||
|
||||
private updateChartWithFilters(filterValues: any): void {
|
||||
if (this.componentRef) {
|
||||
// Add filter values to chart inputs
|
||||
const updatedInputs = {
|
||||
...this.chartInputs,
|
||||
filterValues: filterValues,
|
||||
// Pass the query params string for easy API integration
|
||||
filterQueryParams: this.filterService.buildQueryParams()
|
||||
};
|
||||
|
||||
// Update chart component inputs
|
||||
Object.keys(updatedInputs).forEach(key => {
|
||||
this.componentRef!.instance[key] = updatedInputs[key];
|
||||
});
|
||||
|
||||
// Trigger change detection if the component has a method for it
|
||||
if (this.componentRef!.instance.ngOnChanges) {
|
||||
// We can't easily trigger ngOnChanges manually, but the input update should trigger it
|
||||
}
|
||||
|
||||
// If the chart component has a method to refresh data, call it
|
||||
if (this.componentRef!.instance.refreshData) {
|
||||
this.componentRef!.instance.refreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<div class="common-filter-container">
|
||||
<!-- Filter Header -->
|
||||
<div class="filter-header">
|
||||
<h4>Common Filters</h4>
|
||||
<button class="btn btn-sm btn-primary" (click)="addFilter()">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Presets Section -->
|
||||
<div class="presets-section" *ngIf="presets.length > 0">
|
||||
<div class="preset-controls">
|
||||
<select [(ngModel)]="activePreset" (change)="loadPreset(activePreset || '')" class="clr-select">
|
||||
<option value="">Select Preset</option>
|
||||
<option *ngFor="let preset of presets" [value]="preset">{{ preset }}</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-danger" (click)="resetFilters()">
|
||||
<clr-icon shape="undo"></clr-icon> Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Preset Section -->
|
||||
<div class="save-preset-section">
|
||||
<div class="clr-input-group">
|
||||
<input type="text" [(ngModel)]="newPresetName" placeholder="Preset name" class="clr-input">
|
||||
<div class="clr-input-group-btn">
|
||||
<button class="btn btn-sm btn-success" (click)="savePreset()" [disabled]="!newPresetName.trim()">
|
||||
<clr-icon shape="floppy"></clr-icon> Save Preset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Form -->
|
||||
<form [formGroup]="filterForm" class="filters-form">
|
||||
<div class="filters-grid">
|
||||
<div *ngFor="let filter of filters" class="filter-item">
|
||||
<div class="filter-header">
|
||||
<span class="filter-label">{{ filter.label }}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeFilter(filter.id)">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Type Selector -->
|
||||
<div class="clr-form-control">
|
||||
<select [(ngModel)]="filter.type" (ngModelChange)="updateFilter(filter.id, 'type', $event)"
|
||||
[ngModelOptions]="{standalone: true}" class="clr-select filter-type-select">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="multiselect">Multi-Select</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="toggle">Toggle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Filter Label Input -->
|
||||
<div class="clr-form-control">
|
||||
<input type="text" [(ngModel)]="filter.label" (ngModelChange)="updateFilter(filter.id, 'label', $event)"
|
||||
[ngModelOptions]="{standalone: true}" placeholder="Filter Label" class="clr-input">
|
||||
</div>
|
||||
|
||||
<!-- Filter Field Input -->
|
||||
<div class="clr-form-control">
|
||||
<input type="text" [(ngModel)]="filter.field" (ngModelChange)="updateFilter(filter.id, 'field', $event)"
|
||||
[ngModelOptions]="{standalone: true}" placeholder="Field Name" class="clr-input">
|
||||
</div>
|
||||
|
||||
<!-- Filter Options (for dropdown and multiselect) -->
|
||||
<div class="clr-form-control" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
|
||||
<textarea [(ngModel)]="filter.options" (ngModelChange)="updateFilter(filter.id, 'options', $event ? $event.split(',') : [])"
|
||||
[ngModelOptions]="{standalone: true}" placeholder="Options (comma separated)" class="clr-textarea"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filter Controls Based on Type -->
|
||||
<div class="filter-control" [ngSwitch]="filter.type">
|
||||
<!-- Text Filter -->
|
||||
<div *ngSwitchCase="'text'">
|
||||
<input type="text" [formControlName]="filter.id" placeholder="Enter text" class="clr-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngSwitchCase="'dropdown'">
|
||||
<select [formControlName]="filter.id" class="clr-select">
|
||||
<option value="">Select an option</option>
|
||||
<option *ngFor="let option of filter.options" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter -->
|
||||
<div *ngSwitchCase="'multiselect'">
|
||||
<select [formControlName]="filter.id" multiple class="clr-select multiselect">
|
||||
<option *ngFor="let option of filter.options" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngSwitchCase="'date-range'" class="date-range-controls">
|
||||
<div class="clr-form-control">
|
||||
<label>Start Date</label>
|
||||
<input type="date" [ngModel]="filterForm.get(filter.id + '.start')?.value"
|
||||
(ngModelChange)="onDateRangeChange(filter.id, { start: $event, end: filterForm.get(filter.id + '.end')?.value })"
|
||||
[ngModelOptions]="{standalone: true}" class="clr-input">
|
||||
</div>
|
||||
<div class="clr-form-control">
|
||||
<label>End Date</label>
|
||||
<input type="date" [ngModel]="filterForm.get(filter.id + '.end')?.value"
|
||||
(ngModelChange)="onDateRangeChange(filter.id, { start: filterForm.get(filter.id + '.start')?.value, end: $event })"
|
||||
[ngModelOptions]="{standalone: true}" class="clr-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngSwitchCase="'toggle'" class="toggle-control">
|
||||
<input type="checkbox" [formControlName]="filter.id" clrToggle class="clr-toggle">
|
||||
<label>{{ filter.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- No Filters Message -->
|
||||
<div class="no-filters" *ngIf="filters.length === 0">
|
||||
<p>No filters added yet. Click "Add Filter" to create your first filter.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,192 @@
|
||||
.common-filter-container {
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.presets-section {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.preset-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
select {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.save-preset-section {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.clr-input-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.clr-input {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.clr-input-group-btn {
|
||||
.btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters-form {
|
||||
.filters-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
|
||||
.filter-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.filter-label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-type-select {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.clr-form-control {
|
||||
margin-bottom: 10px;
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
min-width: 0; // Allow flexbox to shrink items
|
||||
}
|
||||
}
|
||||
|
||||
.date-range-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
|
||||
.clr-form-control {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-filters {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design for smaller screens
|
||||
@media (max-width: 768px) {
|
||||
.common-filter-container {
|
||||
padding: 10px;
|
||||
|
||||
.filters-form {
|
||||
.filters-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.presets-section {
|
||||
.preset-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.save-preset-section {
|
||||
.clr-input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.common-filter-container {
|
||||
.date-range-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { CommonFilterComponent } from './common-filter.component';
|
||||
import { FilterService } from './filter.service';
|
||||
|
||||
describe('CommonFilterComponent', () => {
|
||||
let component: CommonFilterComponent;
|
||||
let fixture: ComponentFixture<CommonFilterComponent>;
|
||||
let filterService: FilterService;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
declarations: [CommonFilterComponent],
|
||||
providers: [FormBuilder, FilterService]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommonFilterComponent);
|
||||
component = fixture.componentInstance;
|
||||
filterService = TestBed.inject(FilterService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add a new filter', () => {
|
||||
const initialFilters = filterService.getFilters().length;
|
||||
component.addFilter();
|
||||
const updatedFilters = filterService.getFilters().length;
|
||||
expect(updatedFilters).toBe(initialFilters + 1);
|
||||
});
|
||||
|
||||
it('should remove a filter', () => {
|
||||
// Add a filter first
|
||||
component.addFilter();
|
||||
const filterId = filterService.getFilters()[0].id;
|
||||
|
||||
// Then remove it
|
||||
const initialFilters = filterService.getFilters().length;
|
||||
component.removeFilter(filterId);
|
||||
const updatedFilters = filterService.getFilters().length;
|
||||
expect(updatedFilters).toBe(initialFilters - 1);
|
||||
});
|
||||
|
||||
it('should update filter properties', () => {
|
||||
// Add a filter
|
||||
component.addFilter();
|
||||
const filter = filterService.getFilters()[0];
|
||||
|
||||
// Update the filter label
|
||||
component.updateFilter(filter.id, 'label', 'Updated Label');
|
||||
const updatedFilters = filterService.getFilters();
|
||||
expect(updatedFilters[0].label).toBe('Updated Label');
|
||||
});
|
||||
|
||||
it('should handle filter value changes', () => {
|
||||
const filterId = 'test-filter';
|
||||
const testValue = 'test value';
|
||||
|
||||
// Mock the filter service method
|
||||
spyOn(filterService, 'updateFilterValue');
|
||||
|
||||
component.onFilterChange(filterId, testValue);
|
||||
expect(filterService.updateFilterValue).toHaveBeenCalledWith(filterId, testValue);
|
||||
});
|
||||
|
||||
it('should reset filters', () => {
|
||||
spyOn(filterService, 'resetFilters');
|
||||
component.resetFilters();
|
||||
expect(filterService.resetFilters).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save and load presets', () => {
|
||||
spyOn(filterService, 'savePreset');
|
||||
spyOn(filterService, 'loadPreset');
|
||||
|
||||
// Test save preset
|
||||
component.newPresetName = 'Test Preset';
|
||||
component.savePreset();
|
||||
expect(filterService.savePreset).toHaveBeenCalledWith('Test Preset');
|
||||
|
||||
// Test load preset
|
||||
component.loadPreset('Test Preset');
|
||||
expect(filterService.loadPreset).toHaveBeenCalledWith('Test Preset');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Component, OnInit, OnDestroy, Input, HostListener } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Filter, FilterService, FilterType } from './filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-common-filter',
|
||||
templateUrl: './common-filter.component.html',
|
||||
styleUrls: ['./common-filter.component.scss']
|
||||
})
|
||||
export class CommonFilterComponent implements OnInit, OnDestroy {
|
||||
@Input() baseFilters: any[] = [];
|
||||
@Input() drilldownFilters: any[] = [];
|
||||
@Input() drilldownLayers: any[] = [];
|
||||
@Input() fieldName: string;
|
||||
@Input() connection: number;
|
||||
|
||||
filters: Filter[] = [];
|
||||
filterForm: FormGroup;
|
||||
presets: string[] = [];
|
||||
activePreset: string | null = null;
|
||||
newPresetName: string = '';
|
||||
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private filterService: FilterService,
|
||||
private fb: FormBuilder
|
||||
) {
|
||||
this.filterForm = this.fb.group({});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter definitions
|
||||
this.subscriptions.push(
|
||||
this.filterService.filters$.subscribe(filters => {
|
||||
this.filters = filters;
|
||||
this.buildForm();
|
||||
})
|
||||
);
|
||||
|
||||
// Subscribe to filter state changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(state => {
|
||||
this.updateFormValues(state);
|
||||
})
|
||||
);
|
||||
|
||||
// Subscribe to preset changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.activePreset$.subscribe(preset => {
|
||||
this.activePreset = preset;
|
||||
})
|
||||
);
|
||||
|
||||
// Get initial presets
|
||||
this.presets = this.filterService.getPresets();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
// Handle window resize events
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event: any) {
|
||||
// Trigger change detection to reflow the layout
|
||||
setTimeout(() => {
|
||||
// This will cause the grid to recalculate its layout
|
||||
this.filters = [...this.filters];
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Build the form based on current filters
|
||||
private buildForm(): void {
|
||||
// Clear existing form controls
|
||||
Object.keys(this.filterForm.controls).forEach(key => {
|
||||
this.filterForm.removeControl(key);
|
||||
});
|
||||
|
||||
// Add controls for each filter
|
||||
this.filters.forEach(filter => {
|
||||
let initialValue: any;
|
||||
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
initialValue = filter.value || [];
|
||||
break;
|
||||
case 'date-range':
|
||||
initialValue = filter.value || { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
initialValue = filter.value || false;
|
||||
break;
|
||||
default:
|
||||
initialValue = filter.value || '';
|
||||
}
|
||||
|
||||
const control = this.fb.control(initialValue);
|
||||
|
||||
// Subscribe to value changes for this control
|
||||
control.valueChanges.subscribe(value => {
|
||||
this.onFilterChange(filter.id, value);
|
||||
});
|
||||
|
||||
this.filterForm.addControl(filter.id, control);
|
||||
});
|
||||
}
|
||||
|
||||
// Update form values based on filter state
|
||||
private updateFormValues(state: any): void {
|
||||
Object.keys(state).forEach(key => {
|
||||
if (this.filterForm.contains(key)) {
|
||||
this.filterForm.get(key)?.setValue(state[key], { emitEvent: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle filter value changes
|
||||
onFilterChange(filterId: string, value: any): void {
|
||||
console.log('=== COMMON FILTER DEBUG INFO ===');
|
||||
console.log('Filter value changed for ID:', filterId);
|
||||
console.log('New value:', value);
|
||||
|
||||
const filterDef = this.filters.find(f => f.id === filterId);
|
||||
console.log('Filter definition:', filterDef);
|
||||
|
||||
this.filterService.updateFilterValue(filterId, value);
|
||||
console.log('=== END COMMON FILTER DEBUG ===');
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiselectChange(filterId: string, selectedValues: string[]): void {
|
||||
this.filterService.updateFilterValue(filterId, selectedValues);
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filterId: string, dateRange: { start: string | null, end: string | null }): void {
|
||||
this.filterService.updateFilterValue(filterId, dateRange);
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filterId: string, checked: boolean): void {
|
||||
this.filterService.updateFilterValue(filterId, checked);
|
||||
}
|
||||
|
||||
// Reset all filters
|
||||
resetFilters(): void {
|
||||
this.filterService.resetFilters();
|
||||
}
|
||||
|
||||
// Save current filter state as preset
|
||||
savePreset(): void {
|
||||
if (this.newPresetName.trim()) {
|
||||
this.filterService.savePreset(this.newPresetName.trim());
|
||||
this.presets = this.filterService.getPresets();
|
||||
this.newPresetName = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Load a preset
|
||||
loadPreset(presetName: string): void {
|
||||
this.filterService.loadPreset(presetName);
|
||||
}
|
||||
|
||||
// Delete a preset
|
||||
deletePreset(presetName: string): void {
|
||||
this.filterService.deletePreset(presetName);
|
||||
this.presets = this.filterService.getPresets();
|
||||
}
|
||||
|
||||
// Add a new filter
|
||||
addFilter(): void {
|
||||
const newFilter: Filter = {
|
||||
id: `filter_${Date.now()}`,
|
||||
field: '',
|
||||
label: 'New Filter',
|
||||
type: 'text'
|
||||
};
|
||||
this.filterService.addFilter(newFilter);
|
||||
}
|
||||
|
||||
// Remove a filter
|
||||
removeFilter(filterId: string): void {
|
||||
this.filterService.removeFilter(filterId);
|
||||
}
|
||||
|
||||
// Update filter properties
|
||||
updateFilter(filterId: string, property: string, value: any): void {
|
||||
const filterIndex = this.filters.findIndex(f => f.id === filterId);
|
||||
if (filterIndex !== -1) {
|
||||
const updatedFilters = [...this.filters];
|
||||
(updatedFilters[filterIndex] as any)[property] = value;
|
||||
this.filterService.setFilters(updatedFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<!-- Configuration Mode -->
|
||||
<div class="compact-filter-config" *ngIf="isConfigMode">
|
||||
<div class="config-header">
|
||||
<h5>Compact Filter Configuration</h5>
|
||||
<button class="btn btn-sm btn-link" (click)="cancelConfiguration()">
|
||||
<clr-icon shape="close"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="config-form">
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">API URL</label>
|
||||
<input type="text" [(ngModel)]="configApiUrl" (ngModelChange)="onApiUrlChange($event)" placeholder="Enter API URL" class="clr-input">
|
||||
</div>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">Filter Key</label>
|
||||
<select [(ngModel)]="configFilterKey" (ngModelChange)="onFilterKeyChange($event)" class="clr-select">
|
||||
<option value="">Select a key</option>
|
||||
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">Filter Type</label>
|
||||
<select [(ngModel)]="configFilterType" (ngModelChange)="onFilterTypeChange($event)" class="clr-select">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="multiselect">Multi-Select</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="toggle">Toggle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Options will be automatically populated for dropdown/multiselect based on API data -->
|
||||
<div class="clr-form-control" *ngIf="configFilterType === 'dropdown' || configFilterType === 'multiselect'">
|
||||
<label class="clr-control-label">Available Values (comma separated)</label>
|
||||
<div class="available-values">
|
||||
{{ availableValues.join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="cancelConfiguration()">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary" (click)="saveConfiguration()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Mode -->
|
||||
<div class="compact-filter" *ngIf="!isConfigMode">
|
||||
<div class="filter-header">
|
||||
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
|
||||
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
|
||||
<span class="filter-type">({{ filterType }})</span>
|
||||
<button class="btn btn-icon btn-sm" (click)="toggleConfigMode()">
|
||||
<clr-icon shape="cog"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'text'">
|
||||
<input type="text"
|
||||
[(ngModel)]="filterValue"
|
||||
(ngModelChange)="onFilterValueChange($event)"
|
||||
[placeholder]="filterLabel || filterKey"
|
||||
class="clr-input compact-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'dropdown'">
|
||||
<select [(ngModel)]="filterValue"
|
||||
(ngModelChange)="onFilterValueChange($event)"
|
||||
class="clr-select compact-select">
|
||||
<option value="">{{ filterLabel || filterKey }}</option>
|
||||
<option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter -->
|
||||
<div class="filter-control" *ngIf="filterType === 'multiselect'">
|
||||
<div class="compact-multiselect-display" (click)="toggleMultiselectDropdown()" style="padding: 5px; border: 1px solid #ddd; cursor: pointer; background-color: #f8f8f8;">
|
||||
<span *ngIf="filterValue && filterValue.length > 0">{{ filterValue.length }} selected</span>
|
||||
<span *ngIf="!filterValue || filterValue.length === 0">{{ filterLabel || filterKey || 'Select options' }}</span>
|
||||
<clr-icon shape="caret down" style="float: right; margin-top: 3px;"></clr-icon>
|
||||
</div>
|
||||
<div class="compact-multiselect-dropdown" *ngIf="showMultiselectDropdown" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; border-top: none; padding: 10px; background-color: white; position: absolute; z-index: 1000; width: calc(100% - 2px); box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;">
|
||||
<input type="checkbox"
|
||||
[id]="'multiselect-' + option"
|
||||
[value]="option"
|
||||
[checked]="isOptionSelected(option)"
|
||||
(change)="onMultiselectOptionChange($event, option)">
|
||||
<label [for]="'multiselect-' + option" class="clr-control-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.start"
|
||||
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input compact-date">
|
||||
<input type="date"
|
||||
[(ngModel)]="filterValue.end"
|
||||
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input compact-date">
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div class="filter-control toggle" *ngIf="filterType === 'toggle'">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filterValue"
|
||||
(ngModelChange)="onToggleChange($event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filterLabel || filterKey }}</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,224 @@
|
||||
.compact-filter {
|
||||
display: inline-block;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.filter-label, .filter-key {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.filter-type {
|
||||
font-size: 10px;
|
||||
color: #6c757d;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 2px;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
&.date-range {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.toggle {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-multiselect,
|
||||
.compact-date {
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.compact-select,
|
||||
.compact-multiselect {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.compact-multiselect {
|
||||
height: auto;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.clr-checkbox {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.clr-toggle {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
min-width: 100px;
|
||||
max-width: 150px;
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-multiselect,
|
||||
.compact-date {
|
||||
font-size: 11px;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 11px;
|
||||
max-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
min-width: 80px;
|
||||
max-width: 120px;
|
||||
|
||||
.compact-input,
|
||||
.compact-select,
|
||||
.compact-multiselect,
|
||||
.compact-date {
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 10px;
|
||||
max-width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compact-filter-config {
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
padding: 2px;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.config-form {
|
||||
.clr-form-control {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.clr-control-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.clr-input,
|
||||
.clr-select,
|
||||
.clr-textarea {
|
||||
font-size: 12px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.clr-textarea {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.available-values {
|
||||
background: #e9ecef;
|
||||
border-radius: 3px;
|
||||
padding: 6px;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
|
||||
.btn {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
|
||||
import { FilterService, Filter } from './filter.service';
|
||||
import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-compact-filter',
|
||||
templateUrl: './compact-filter.component.html',
|
||||
styleUrls: ['./compact-filter.component.scss']
|
||||
})
|
||||
export class CompactFilterComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() filterKey: string = '';
|
||||
@Input() filterType: string = 'text';
|
||||
@Input() filterOptions: string[] = [];
|
||||
@Input() filterLabel: string = '';
|
||||
@Input() apiUrl: string = '';
|
||||
@Input() connectionId: number | undefined;
|
||||
@Output() filterChange = new EventEmitter<any>();
|
||||
@Output() configChange = new EventEmitter<any>();
|
||||
|
||||
selectedFilter: Filter | null = null;
|
||||
filterValue: any = '';
|
||||
availableFilters: Filter[] = [];
|
||||
availableKeys: string[] = [];
|
||||
availableValues: string[] = [];
|
||||
|
||||
// Multiselect dropdown state
|
||||
showMultiselectDropdown: boolean = false;
|
||||
|
||||
// Configuration properties
|
||||
isConfigMode: boolean = false;
|
||||
configFilterKey: string = '';
|
||||
configFilterType: string = 'text';
|
||||
configFilterOptions: string = '';
|
||||
configFilterLabel: string = '';
|
||||
configApiUrl: string = '';
|
||||
configConnectionId: number | undefined;
|
||||
|
||||
constructor(
|
||||
private filterService: FilterService,
|
||||
private alertService: AlertsService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize configuration from inputs
|
||||
this.configFilterKey = this.filterKey;
|
||||
this.configFilterType = this.filterType;
|
||||
this.configFilterLabel = this.filterLabel;
|
||||
this.configFilterOptions = this.filterOptions.join(',');
|
||||
this.configApiUrl = this.apiUrl;
|
||||
this.configConnectionId = this.connectionId;
|
||||
|
||||
// Load available keys and values if API URL and filter key are provided
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
// Load available values for the current filter key if it's a dropdown or multiselect
|
||||
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
|
||||
this.loadAvailableValues(this.filterKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Subscribe to filter definitions to get available filters
|
||||
this.filterService.filters$.subscribe(filters => {
|
||||
this.availableFilters = filters;
|
||||
this.updateSelectedFilter();
|
||||
});
|
||||
|
||||
// Subscribe to filter state changes
|
||||
this.filterService.filterState$.subscribe(state => {
|
||||
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
|
||||
this.filterValue = state[this.selectedFilter.id];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// If filterKey changes, clear the previous filter value and remove old filter from service
|
||||
if (changes.filterKey) {
|
||||
// Clear the previous filter value
|
||||
this.filterValue = '';
|
||||
|
||||
// Clear filter options
|
||||
this.filterOptions = [];
|
||||
|
||||
// Clear available values
|
||||
this.availableValues = [];
|
||||
|
||||
// If we had a previous selected filter, clear its value in the service
|
||||
if (this.selectedFilter && changes.filterKey.previousValue) {
|
||||
const oldFilterId = changes.filterKey.previousValue;
|
||||
this.filterService.updateFilterValue(oldFilterId, '');
|
||||
}
|
||||
}
|
||||
|
||||
// If filterKey or filterType changes, re-register the filter
|
||||
if (changes.filterKey || changes.filterType) {
|
||||
// Load available values for the current filter key if it's a dropdown or multiselect
|
||||
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
|
||||
this.loadAvailableValues(this.filterKey);
|
||||
}
|
||||
this.registerFilter();
|
||||
}
|
||||
|
||||
// Handle API URL changes
|
||||
if (changes.apiUrl && !changes.apiUrl.firstChange) {
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register this filter with the filter service
|
||||
registerFilter(): void {
|
||||
if (this.filterKey) {
|
||||
// Get current filter values from the service
|
||||
const currentFilterValues = this.filterService.getFilterValues();
|
||||
|
||||
// Create a filter definition for this compact filter
|
||||
const filterDef: Filter = {
|
||||
id: `${this.filterKey}`,
|
||||
field: this.filterKey,
|
||||
label: this.filterLabel || this.filterKey,
|
||||
type: this.filterType as any,
|
||||
options: this.filterOptions,
|
||||
value: this.filterValue // Use the current filter value
|
||||
};
|
||||
|
||||
// Get current filters
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
|
||||
// Check if this filter is already registered
|
||||
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
|
||||
|
||||
if (existingFilterIndex >= 0) {
|
||||
// Preserve the existing filter configuration
|
||||
const existingFilter = currentFilters[existingFilterIndex];
|
||||
|
||||
// Preserve the existing filter value if it exists in the service
|
||||
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
|
||||
filterDef.value = currentFilterValues[existingFilter.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
} else if (existingFilter.value !== undefined) {
|
||||
// Fallback to existing filter's value if no service value
|
||||
filterDef.value = existingFilter.value;
|
||||
this.filterValue = filterDef.value;
|
||||
}
|
||||
|
||||
// Preserve other configuration properties
|
||||
filterDef.label = existingFilter.label;
|
||||
filterDef.options = existingFilter.options || this.filterOptions;
|
||||
|
||||
// Update existing filter
|
||||
currentFilters[existingFilterIndex] = filterDef;
|
||||
} else {
|
||||
// For new filters, check if there's already a value in the service
|
||||
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
|
||||
filterDef.value = currentFilterValues[filterDef.id];
|
||||
this.filterValue = filterDef.value; // Update local value
|
||||
}
|
||||
|
||||
// Add new filter
|
||||
currentFilters.push(filterDef);
|
||||
}
|
||||
|
||||
// Update the filter service with the new filter list
|
||||
this.filterService.setFilters(currentFilters);
|
||||
|
||||
// Update the selected filter reference
|
||||
this.selectedFilter = filterDef;
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedFilter(): void {
|
||||
if (this.filterKey && this.availableFilters.length > 0) {
|
||||
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
|
||||
if (this.selectedFilter) {
|
||||
// Get current value for this filter from the service
|
||||
const currentState = this.filterService.getFilterValues();
|
||||
const filterValue = currentState[this.selectedFilter.id];
|
||||
if (filterValue !== undefined) {
|
||||
this.filterValue = filterValue;
|
||||
} else if (this.selectedFilter.value !== undefined) {
|
||||
// Use the filter's default value if no service value
|
||||
this.filterValue = this.selectedFilter.value;
|
||||
} else {
|
||||
// Use the current filter value as fallback
|
||||
this.filterValue = this.filterValue || '';
|
||||
}
|
||||
|
||||
// Also update configuration properties from the selected filter
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFilterValueChange(value: any): void {
|
||||
if (this.selectedFilter) {
|
||||
this.filterValue = value;
|
||||
this.filterService.updateFilterValue(this.selectedFilter.id, value);
|
||||
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
|
||||
|
||||
// Update the filter definition in the service to reflect the new value
|
||||
const currentFilters = this.filterService.getFilters();
|
||||
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
|
||||
if (filterIndex >= 0) {
|
||||
currentFilters[filterIndex].value = value;
|
||||
this.filterService.setFilters(currentFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onToggleChange(checked: boolean): void {
|
||||
this.onFilterValueChange(checked);
|
||||
}
|
||||
|
||||
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
|
||||
this.onFilterValueChange(dateRange);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Component cleanup - remove this filter from the filter service
|
||||
if (this.selectedFilter) {
|
||||
// Use the proper removeFilter method which handles both filter definition and state
|
||||
this.filterService.removeFilter(this.selectedFilter.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Load available keys from API
|
||||
loadAvailableKeys(): void {
|
||||
if (this.apiUrl) {
|
||||
this.alertService.getColumnfromurl(this.apiUrl, this.connectionId).subscribe(
|
||||
(keys: string[]) => {
|
||||
this.availableKeys = keys;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available keys:', error);
|
||||
this.availableKeys = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load available values for a specific key
|
||||
loadAvailableValues(key: string): void {
|
||||
if (this.apiUrl && key) {
|
||||
this.alertService.getValuesFromUrl(this.apiUrl, this.connectionId, key).subscribe(
|
||||
(values: string[]) => {
|
||||
this.availableValues = values;
|
||||
// Update filter options if this is a dropdown or multiselect
|
||||
if (this.filterType === 'dropdown' || this.filterType === 'multiselect') {
|
||||
this.filterOptions = values;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading available values:', error);
|
||||
this.availableValues = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration methods
|
||||
toggleConfigMode(): void {
|
||||
this.isConfigMode = !this.isConfigMode;
|
||||
if (this.isConfigMode) {
|
||||
// Initialize config values from current filter if available
|
||||
if (this.selectedFilter) {
|
||||
this.configFilterKey = this.selectedFilter.field;
|
||||
this.configFilterType = this.selectedFilter.type;
|
||||
this.configFilterLabel = this.selectedFilter.label;
|
||||
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
|
||||
} else {
|
||||
// Fallback to current properties
|
||||
this.configFilterKey = this.filterKey;
|
||||
this.configFilterType = this.filterType;
|
||||
this.configFilterLabel = this.filterLabel;
|
||||
this.configFilterOptions = this.filterOptions.join(',');
|
||||
}
|
||||
this.configApiUrl = this.apiUrl;
|
||||
this.configConnectionId = this.connectionId;
|
||||
}
|
||||
}
|
||||
|
||||
saveConfiguration(): void {
|
||||
const config = {
|
||||
filterKey: this.configFilterKey,
|
||||
filterType: this.configFilterType,
|
||||
filterLabel: this.configFilterLabel,
|
||||
filterOptions: this.configFilterOptions.split(',').map(opt => opt.trim()).filter(opt => opt),
|
||||
apiUrl: this.configApiUrl,
|
||||
connectionId: this.configConnectionId
|
||||
};
|
||||
|
||||
// Emit configuration change
|
||||
this.configChange.emit(config);
|
||||
|
||||
// Update local properties
|
||||
this.filterKey = config.filterKey;
|
||||
this.filterType = config.filterType;
|
||||
this.filterLabel = config.filterLabel;
|
||||
this.filterOptions = config.filterOptions;
|
||||
this.apiUrl = config.apiUrl;
|
||||
this.connectionId = config.connectionId;
|
||||
|
||||
// Clear filter value when changing configuration
|
||||
this.filterValue = '';
|
||||
|
||||
// Load available keys if API URL is provided
|
||||
if (this.apiUrl) {
|
||||
this.loadAvailableKeys();
|
||||
}
|
||||
|
||||
// Load available values for the selected key if it's a dropdown or multiselect
|
||||
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && this.configFilterKey) {
|
||||
this.loadAvailableValues(this.configFilterKey);
|
||||
}
|
||||
|
||||
// Register the updated filter with the filter service
|
||||
this.registerFilter();
|
||||
|
||||
// Update selected filter
|
||||
this.updateSelectedFilter();
|
||||
|
||||
// Exit config mode
|
||||
this.isConfigMode = false;
|
||||
}
|
||||
|
||||
cancelConfiguration(): void {
|
||||
this.isConfigMode = false;
|
||||
}
|
||||
|
||||
// Handle filter key change in configuration
|
||||
onFilterKeyChange(key: string): void {
|
||||
// Clear the previous filter value when changing keys
|
||||
this.filterValue = '';
|
||||
|
||||
// Clear filter options until new values are loaded
|
||||
this.filterOptions = [];
|
||||
|
||||
this.configFilterKey = key;
|
||||
|
||||
// Load available values for the selected key if it's a dropdown or multiselect
|
||||
if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && key) {
|
||||
this.loadAvailableValues(key);
|
||||
}
|
||||
|
||||
// Clear the filter service value for the previous filter key
|
||||
if (this.selectedFilter) {
|
||||
this.filterService.updateFilterValue(this.selectedFilter.id, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API URL change in configuration
|
||||
onApiUrlChange(url: string): void {
|
||||
this.configApiUrl = url;
|
||||
// Load available keys when API URL changes
|
||||
if (url) {
|
||||
this.loadAvailableKeys();
|
||||
// Also clear available values since the API has changed
|
||||
this.availableValues = [];
|
||||
this.filterOptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle filter type change in configuration
|
||||
onFilterTypeChange(type: string): void {
|
||||
this.configFilterType = type;
|
||||
// If changing to dropdown or multiselect and we have a key selected, load values
|
||||
if ((type === 'dropdown' || type === 'multiselect') && this.configFilterKey) {
|
||||
this.loadAvailableValues(this.configFilterKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to check if an option is selected for checkboxes
|
||||
isOptionSelected(option: string): boolean {
|
||||
if (!this.filterValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array for multiselect
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.filterValue.includes(option);
|
||||
}
|
||||
// need to check this
|
||||
// Add method to handle multiselect option change
|
||||
onMultiselectOptionChange(event: any, option: string): void {
|
||||
// Initialize filterValue array if it doesn't exist
|
||||
if (!this.filterValue) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
// Ensure filterValue is an array
|
||||
if (!Array.isArray(this.filterValue)) {
|
||||
this.filterValue = [];
|
||||
}
|
||||
|
||||
if (event.target.checked) {
|
||||
// Add option if not already in array
|
||||
if (!this.filterValue.includes(option)) {
|
||||
this.filterValue.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
const index = this.filterValue.indexOf(option);
|
||||
if (index > -1) {
|
||||
this.filterValue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the change event
|
||||
this.onFilterValueChange(this.filterValue);
|
||||
}
|
||||
|
||||
// Add method to toggle multiselect dropdown visibility
|
||||
toggleMultiselectDropdown(): void {
|
||||
this.showMultiselectDropdown = !this.showMultiselectDropdown;
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
if (this.showMultiselectDropdown) {
|
||||
setTimeout(() => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.compact-multiselect-display') && !target.closest('.compact-multiselect-dropdown')) {
|
||||
this.showMultiselectDropdown = false;
|
||||
document.removeEventListener('click', handleClick);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClick);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { FilterService, Filter } from './filter.service';
|
||||
|
||||
describe('FilterService', () => {
|
||||
let service: FilterService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(FilterService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add and remove filters', () => {
|
||||
const filter: Filter = {
|
||||
id: 'test-filter',
|
||||
field: 'testField',
|
||||
label: 'Test Field',
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
// Add filter
|
||||
service.addFilter(filter);
|
||||
const filters = service.getFilters();
|
||||
expect(filters.length).toBe(1);
|
||||
expect(filters[0]).toEqual(filter);
|
||||
|
||||
// Remove filter
|
||||
service.removeFilter('test-filter');
|
||||
const updatedFilters = service.getFilters();
|
||||
expect(updatedFilters.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should update filter values', () => {
|
||||
const filter: Filter = {
|
||||
id: 'name-filter',
|
||||
field: 'name',
|
||||
label: 'Name',
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
service.addFilter(filter);
|
||||
service.updateFilterValue('name-filter', 'John Doe');
|
||||
|
||||
const filterValues = service.getFilterValues();
|
||||
expect(filterValues['name-filter']).toBe('John Doe');
|
||||
});
|
||||
|
||||
it('should reset filters', () => {
|
||||
const textFilter: Filter = {
|
||||
id: 'name',
|
||||
field: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
value: 'John'
|
||||
};
|
||||
|
||||
const toggleFilter: Filter = {
|
||||
id: 'active',
|
||||
field: 'isActive',
|
||||
label: 'Active',
|
||||
type: 'toggle',
|
||||
value: true
|
||||
};
|
||||
|
||||
service.setFilters([textFilter, toggleFilter]);
|
||||
service.resetFilters();
|
||||
|
||||
const filterValues = service.getFilterValues();
|
||||
expect(filterValues['name']).toBe('');
|
||||
expect(filterValues['active']).toBe(false);
|
||||
});
|
||||
|
||||
it('should manage presets', () => {
|
||||
const filter: Filter = {
|
||||
id: 'category',
|
||||
field: 'category',
|
||||
label: 'Category',
|
||||
type: 'dropdown',
|
||||
value: 'Electronics'
|
||||
};
|
||||
|
||||
service.addFilter(filter);
|
||||
|
||||
// Save preset
|
||||
service.savePreset('Electronics View');
|
||||
const presets = service.getPresets();
|
||||
expect(presets).toContain('Electronics View');
|
||||
|
||||
// Update filter and load preset
|
||||
service.updateFilterValue('category', 'Clothing');
|
||||
service.loadPreset('Electronics View');
|
||||
|
||||
const filterValues = service.getFilterValues();
|
||||
expect(filterValues['category']).toBe('Electronics');
|
||||
|
||||
// Delete preset
|
||||
service.deletePreset('Electronics View');
|
||||
const updatedPresets = service.getPresets();
|
||||
expect(updatedPresets).not.toContain('Electronics View');
|
||||
});
|
||||
|
||||
it('should build query parameters', () => {
|
||||
const textFilter: Filter = {
|
||||
id: 'name',
|
||||
field: 'name',
|
||||
label: 'Name',
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
const dateFilter: Filter = {
|
||||
id: 'dateRange',
|
||||
field: 'date',
|
||||
label: 'Date Range',
|
||||
type: 'date-range'
|
||||
};
|
||||
|
||||
service.setFilters([textFilter, dateFilter]);
|
||||
service.updateFilterValue('name', 'John Doe');
|
||||
service.updateFilterValue('dateRange', { start: '2023-01-01', end: '2023-12-31' });
|
||||
|
||||
const queryParams = service.buildQueryParams();
|
||||
expect(queryParams).toContain('name=John%20Doe');
|
||||
expect(queryParams).toContain('dateRange_start=2023-01-01');
|
||||
expect(queryParams).toContain('dateRange_end=2023-12-31');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
// Define the filter types
|
||||
export type FilterType = 'dropdown' | 'multiselect' | 'date-range' | 'text' | 'toggle';
|
||||
|
||||
// Define the filter interface
|
||||
export interface Filter {
|
||||
id: string;
|
||||
field: string;
|
||||
label: string;
|
||||
type: FilterType;
|
||||
options?: string[]; // For dropdown and multiselect
|
||||
value?: any; // Current value
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
// Define the filter state
|
||||
export interface FilterState {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FilterService {
|
||||
// Store the filter definitions
|
||||
private filtersSubject = new BehaviorSubject<Filter[]>([]);
|
||||
public filters$ = this.filtersSubject.asObservable();
|
||||
|
||||
// Store the current filter values
|
||||
private filterStateSubject = new BehaviorSubject<FilterState>({});
|
||||
public filterState$ = this.filterStateSubject.asObservable();
|
||||
|
||||
// Store the active filter presets
|
||||
private activePresetSubject = new BehaviorSubject<string | null>(null);
|
||||
public activePreset$ = this.activePresetSubject.asObservable();
|
||||
|
||||
// Store filter presets
|
||||
private presets: { [key: string]: FilterState } = {};
|
||||
|
||||
constructor() { }
|
||||
|
||||
// Add a new filter
|
||||
addFilter(filter: Filter): void {
|
||||
const currentFilters = this.filtersSubject.value;
|
||||
this.filtersSubject.next([...currentFilters, filter]);
|
||||
}
|
||||
|
||||
// Remove a filter
|
||||
removeFilter(filterId: string): void {
|
||||
const currentFilters = this.filtersSubject.value;
|
||||
const updatedFilters = currentFilters.filter(f => f.id !== filterId);
|
||||
this.filtersSubject.next(updatedFilters);
|
||||
|
||||
// Also remove the filter value from state
|
||||
const currentState = this.filterStateSubject.value;
|
||||
const newState = { ...currentState };
|
||||
delete newState[filterId];
|
||||
this.filterStateSubject.next(newState);
|
||||
}
|
||||
|
||||
// Update filter value
|
||||
updateFilterValue(filterId: string, value: any): void {
|
||||
console.log('=== FILTER SERVICE DEBUG INFO ===');
|
||||
console.log('Updating filter value for ID:', filterId);
|
||||
console.log('New value:', value);
|
||||
|
||||
const currentState = this.filterStateSubject.value;
|
||||
const newState = {
|
||||
...currentState,
|
||||
[filterId]: value
|
||||
};
|
||||
|
||||
console.log('New filter state:', newState);
|
||||
this.filterStateSubject.next(newState);
|
||||
console.log('=== END FILTER SERVICE DEBUG ===');
|
||||
}
|
||||
|
||||
// Get current filter values
|
||||
getFilterValues(): FilterState {
|
||||
return this.filterStateSubject.value;
|
||||
}
|
||||
|
||||
// Reset all filters
|
||||
resetFilters(): void {
|
||||
const currentFilters = this.filtersSubject.value;
|
||||
const resetState: FilterState = {};
|
||||
|
||||
// Initialize all filters with empty/default values
|
||||
currentFilters.forEach(filter => {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
resetState[filter.id] = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
resetState[filter.id] = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
resetState[filter.id] = false;
|
||||
break;
|
||||
default:
|
||||
resetState[filter.id] = '';
|
||||
}
|
||||
});
|
||||
|
||||
this.filterStateSubject.next(resetState);
|
||||
}
|
||||
|
||||
// Save current filter state as a preset
|
||||
savePreset(name: string): void {
|
||||
this.presets[name] = this.filterStateSubject.value;
|
||||
}
|
||||
|
||||
// Load a preset
|
||||
loadPreset(name: string): void {
|
||||
if (this.presets[name]) {
|
||||
this.filterStateSubject.next(this.presets[name]);
|
||||
this.activePresetSubject.next(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all presets
|
||||
getPresets(): string[] {
|
||||
return Object.keys(this.presets);
|
||||
}
|
||||
|
||||
// Delete a preset
|
||||
deletePreset(name: string): void {
|
||||
delete this.presets[name];
|
||||
if (this.activePresetSubject.value === name) {
|
||||
this.activePresetSubject.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all presets
|
||||
clearPresets(): void {
|
||||
this.presets = {};
|
||||
this.activePresetSubject.next(null);
|
||||
}
|
||||
|
||||
// Build query parameters for API calls
|
||||
buildQueryParams(): string {
|
||||
const filterValues = this.getFilterValues();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Object.keys(filterValues).forEach(key => {
|
||||
const value = filterValues[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (typeof value === 'object') {
|
||||
// Handle date ranges and other objects
|
||||
if (value.hasOwnProperty('start') && value.hasOwnProperty('end')) {
|
||||
// Date range
|
||||
if (value.start) params.append(`${key}_start`, value.start);
|
||||
if (value.end) params.append(`${key}_end`, value.end);
|
||||
} else {
|
||||
// Other objects as JSON
|
||||
params.append(key, JSON.stringify(value));
|
||||
}
|
||||
} else {
|
||||
// Simple values
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
// Get filter definitions
|
||||
getFilters(): Filter[] {
|
||||
return this.filtersSubject.value;
|
||||
}
|
||||
|
||||
// Update filter definitions
|
||||
setFilters(filters: Filter[]): void {
|
||||
this.filtersSubject.next(filters);
|
||||
|
||||
// Initialize filter state with default values
|
||||
const initialState: FilterState = {};
|
||||
filters.forEach(filter => {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
initialState[filter.id] = filter.value || [];
|
||||
break;
|
||||
case 'date-range':
|
||||
initialState[filter.id] = filter.value || { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
initialState[filter.id] = filter.value || false;
|
||||
break;
|
||||
default:
|
||||
initialState[filter.id] = filter.value || '';
|
||||
}
|
||||
});
|
||||
|
||||
this.filterStateSubject.next(initialState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './filter.service';
|
||||
export * from './common-filter.component';
|
||||
export * from './chart-wrapper.component';
|
||||
export * from './compact-filter.component';
|
||||
@@ -0,0 +1,161 @@
|
||||
# Filter Configuration Guide for Dashboard Editor
|
||||
|
||||
## Overview
|
||||
This guide explains how to configure filters for chart components using the dashboard editor. The filter functionality allows users to dynamically filter chart data using multiple filter types.
|
||||
|
||||
## Enabling Filters
|
||||
|
||||
To enable filters for a chart component:
|
||||
|
||||
1. Open the chart configuration modal by clicking the edit icon on any chart
|
||||
2. Scroll down to the "Filter Configuration" section
|
||||
3. Check the "Enable Filters" checkbox
|
||||
|
||||
## Adding Filter Fields
|
||||
|
||||
Once filters are enabled, you can add filter fields:
|
||||
|
||||
1. Click the "Add Filter Field" button
|
||||
2. Configure each filter field with the following properties:
|
||||
|
||||
### Filter Field Properties
|
||||
|
||||
#### Field Name
|
||||
- The field name to filter on (e.g., 'category', 'name', 'amount')
|
||||
- This should match the field name in your data source
|
||||
|
||||
#### Display Label (Optional)
|
||||
- Label to display for this filter in the UI
|
||||
- If not provided, the field name will be used as the label
|
||||
|
||||
#### Filter Type
|
||||
Choose one of the following filter types:
|
||||
|
||||
1. **Text Input**
|
||||
- Creates a text input field
|
||||
- Users can type to filter data
|
||||
- Works with any text-based field
|
||||
|
||||
2. **Dropdown**
|
||||
- Creates a dropdown selection field
|
||||
- Requires a comma-separated list of options
|
||||
- Example options: "A,B,C" or "North, South, East, West"
|
||||
|
||||
3. **Number Range**
|
||||
- Creates two number input fields (min and max)
|
||||
- Used for filtering numeric data within a range
|
||||
- Example: Filter sales between 100 and 500
|
||||
|
||||
#### Dropdown Options (For Dropdown Type Only)
|
||||
- Comma-separated list of options for dropdown filters
|
||||
- Example: "Option1,Option2,Option3"
|
||||
- Each option will appear as a selectable item in the dropdown
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Example 1: Simple Text Filter
|
||||
```
|
||||
Field Name: name
|
||||
Display Label: Product Name
|
||||
Filter Type: Text Input
|
||||
```
|
||||
|
||||
### Example 2: Category Dropdown Filter
|
||||
```
|
||||
Field Name: category
|
||||
Display Label: Category
|
||||
Filter Type: Dropdown
|
||||
Dropdown Options: A,B,C,D
|
||||
```
|
||||
|
||||
### Example 3: Sales Amount Range Filter
|
||||
```
|
||||
Field Name: amount
|
||||
Display Label: Sales Amount
|
||||
Filter Type: Number Range
|
||||
```
|
||||
|
||||
### Example 4: Multiple Filters
|
||||
You can combine multiple filter types:
|
||||
1. Text filter for product names
|
||||
2. Dropdown filter for categories
|
||||
3. Number range filter for sales amounts
|
||||
|
||||
## Backend Integration
|
||||
|
||||
When filters are applied, they send parameters to your backend API with the prefix `filter_`. For example:
|
||||
- Text filter: `filter_name=John`
|
||||
- Dropdown filter: `filter_category=A`
|
||||
- Number range filter: `filter_amount_min=100&filter_amount_max=500`
|
||||
|
||||
Your backend should implement logic to filter data based on these parameters.
|
||||
|
||||
## Working with Drilldown
|
||||
|
||||
The filter functionality works seamlessly with the existing drilldown feature. Filters will be applied at each drilldown level, allowing users to filter data at any level of the drilldown hierarchy.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Limit the Number of Filters**: Too many filters can overwhelm users. Generally, 3-5 filters are sufficient for most use cases.
|
||||
|
||||
2. **Use Descriptive Labels**: Provide clear, user-friendly labels for filter fields.
|
||||
|
||||
3. **Match Field Names**: Ensure filter field names match the actual field names in your data source.
|
||||
|
||||
4. **Provide Meaningful Dropdown Options**: For dropdown filters, provide options that make sense for your data.
|
||||
|
||||
5. **Test with Real Data**: Always test filter configurations with actual data to ensure they work as expected.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Filters Not Working
|
||||
- Check that field names match your data source
|
||||
- Verify that your backend implements filter parameter handling
|
||||
- Ensure the "Enable Filters" checkbox is checked
|
||||
|
||||
### Dropdown Options Not Displaying
|
||||
- Check that options are comma-separated
|
||||
- Verify there are no extra spaces around commas
|
||||
- Ensure the filter type is set to "Dropdown"
|
||||
|
||||
### Range Filters Not Filtering
|
||||
- Verify that the field contains numeric data
|
||||
- Check that min and max values are entered correctly
|
||||
- Ensure your backend handles range filter parameters
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Data Structure
|
||||
Filter configurations are stored as an array of objects in the chart configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"enableFilters": true,
|
||||
"filterFields": [
|
||||
{
|
||||
"field": "category",
|
||||
"label": "Category",
|
||||
"type": "dropdown",
|
||||
"options": ["A", "B", "C"]
|
||||
},
|
||||
{
|
||||
"field": "name",
|
||||
"label": "Name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"field": "amount",
|
||||
"label": "Amount",
|
||||
"type": "number-range"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### API Parameter Format
|
||||
Filters are passed to the backend as query parameters:
|
||||
- Text filter: `filter_[fieldname]=[value]`
|
||||
- Dropdown filter: `filter_[fieldname]=[selected_value]`
|
||||
- Number range filter: `filter_[fieldname]_min=[min_value]&filter_[fieldname]_max=[max_value]`
|
||||
|
||||
This configuration allows for flexible and powerful data filtering capabilities in your dashboard charts.
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="chart-config-modal">
|
||||
<h3>Chart Configuration Manager</h3>
|
||||
<app-chart-config-manager></app-chart-config-manager>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
.chart-config-modal {
|
||||
padding: 20px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-config-modal',
|
||||
templateUrl: './chart-config-modal.component.html',
|
||||
styleUrls: ['./chart-config-modal.component.scss']
|
||||
})
|
||||
export class ChartConfigModalComponent {
|
||||
// This component will be used to display the chart configuration manager in a modal
|
||||
}
|
||||
@@ -7,116 +7,233 @@
|
||||
</ol> -->
|
||||
|
||||
<div style="display: inline;">
|
||||
<button class="btn componentbtn" (click)="toggleMenu()"><clr-icon shape="plus"></clr-icon>component</button>
|
||||
<div style="display: inline;">
|
||||
{{dashboardName}}
|
||||
</div>
|
||||
<div style="display: inline; float: right;">
|
||||
<!-- <button class="btn btn-primary">Build</button>
|
||||
<button class="btn btn-primary" (click)="onSchedule()">Schedule</button> -->
|
||||
</div>
|
||||
|
||||
<button class="btn componentbtn" (click)="toggleMenu()" *ngIf="!fromRunner"><clr-icon shape="plus"></clr-icon>component</button>
|
||||
<button class="btn btn-primary" (click)="openCommonFilterModal()" style="margin-left: 10px;" *ngIf="!fromRunner">
|
||||
<clr-icon shape="filter"></clr-icon> Common Filter
|
||||
</button>
|
||||
<div style="display: inline;">
|
||||
{{dashboardName}}
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
<nav class="sidenav" *ngIf="toggle" style="width: 16%;">
|
||||
<div style="display: inline; float: right;">
|
||||
<!-- <button class="btn btn-primary">Build</button>
|
||||
<button class="btn btn-primary" (click)="onSchedule()">Schedule</button> -->
|
||||
<button class="btn btn-success" (click)="testDynamicChartCreation()" style="margin-left: 10px;" *ngIf="!fromRunner">
|
||||
<clr-icon shape="test"></clr-icon> Test Dynamic Chart
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
<nav class="sidenav" *ngIf="toggle && !fromRunner" style="width: 16%;">
|
||||
<ul class="nav-list" style="list-style-type: none;">
|
||||
<li *ngFor="let widget of WidgetsMock">
|
||||
|
||||
|
||||
<!--
|
||||
Draggable widget from store using vanilla javascript event (dragstart)
|
||||
onDrag() is call, it take $event and a widget identifier as parameters
|
||||
-->
|
||||
<a draggable="true" class="nav-link" (dragstart)="onDrag($event, widget.identifier)">
|
||||
<clr-icon shape="drag-handle" style="margin-right: 10px;"></clr-icon>
|
||||
{{ widget.name }}
|
||||
{{ widget.name }}
|
||||
<clr-icon shape="plugin" class="has-badge"></clr-icon>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div style="width: 100%;">
|
||||
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent;">
|
||||
<gridster-item [item]="item" *ngFor="let item of dashboardArray">
|
||||
<!-- <ng-container *ngIf="addToDashboard && item.addToDashboard"> -->
|
||||
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;">
|
||||
<clr-icon shape="drag-handle"></clr-icon>
|
||||
</button>
|
||||
<div style="width: 100%;">
|
||||
<gridster [options]="options" (drop)="onDrop($event)" style="background-color: transparent;">
|
||||
<gridster-item [item]="item" *ngFor="let item of dashboardArray">
|
||||
<!-- <ng-container *ngIf="addToDashboard && item.addToDashboard"> -->
|
||||
<button class="btn btn-icon btn-danger" style="margin-left: 10px; margin-top: 10px;" (click)="removeItem(item)" *ngIf="!fromRunner">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
<button class="btn btn-icon drag-handler" style="margin-left: 10px; margin-top: 10px;" *ngIf="!fromRunner">
|
||||
<clr-icon shape="drag-handle"></clr-icon>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-icon" style="margin-top: 10px; float: right;">
|
||||
<input type="checkbox" clrToggle [(ngModel)]="item.addToDashboard" name="addToDashboardSwitch" (change)="toggleAddToDashboard(item)" />
|
||||
</button>
|
||||
<button class="btn btn-icon" style="margin-top: 10px; float: right;" *ngIf="!fromRunner">
|
||||
<input type="checkbox" clrToggle [(ngModel)]="item.addToDashboard" name="addToDashboardSwitch"
|
||||
(change)="toggleAddToDashboard(item)" />
|
||||
</button>
|
||||
|
||||
<!-- <label for="workflow_name">Add to Dasboard</label>
|
||||
<!-- <label for="workflow_name">Add to Dasboard</label>
|
||||
<input class="btn btn-icon" style="margin-top: 10px;float: right;" type="checkbox" clrToggle value="billable" name="billable" />
|
||||
-->
|
||||
<button class="btn btn-icon" style="margin-top: 10px;float: right;" (click)="editGadget(item)">
|
||||
<clr-icon shape="pencil"></clr-icon>
|
||||
</button>
|
||||
<button class="btn btn-icon" style="margin-top: 10px;float: right;" (click)="editGadget(item)" *ngIf="!fromRunner">
|
||||
<clr-icon shape="pencil"></clr-icon>
|
||||
</button>
|
||||
|
||||
<h4 style="margin-top: 0px; margin-left: 10px;">{{item.name}}</h4>
|
||||
<ndc-dynamic class="no-drag" [ndcDynamicComponent]="item.component" (moduleInfo)="display($event)"></ndc-dynamic>
|
||||
<!-- </ng-container> -->
|
||||
</gridster-item>
|
||||
</gridster>
|
||||
</div>
|
||||
<h4 style="margin-top: 0px; margin-left: 10px;">{{item.name}}</h4>
|
||||
<ndc-dynamic class="no-drag" [ndcDynamicComponent]="item.component" [ndcDynamicInputs]="getChartInputs(item)"
|
||||
(moduleInfo)="display($event)"></ndc-dynamic>
|
||||
<!-- </ng-container> -->
|
||||
|
||||
</gridster-item>
|
||||
</gridster>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<button class="btn btn-outline" (click)="goBack()">Back</button>
|
||||
<button type="submit" class="btn btn-primary btn-adddata " (click)="UpdateLine()" >
|
||||
<b>Update</b>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<clr-modal [(clrModalOpen)]="modeledit" [clrModalStaticBackdrop]="true">
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<button class="btn btn-outline" (click)="goBack()">Back</button>
|
||||
<button type="submit" class="btn btn-primary btn-adddata " (click)="UpdateLine()" *ngIf="!fromRunner">
|
||||
<b>Update</b>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<clr-modal [(clrModalOpen)]="modeledit" [clrModalStaticBackdrop]="true">
|
||||
<h3 class="modal-title">Configure Chart</h3>
|
||||
<div class="modal-body" >
|
||||
<form [formGroup]="entryForm" class="clr-form-horizontal" >
|
||||
<div class="modal-body">
|
||||
<form [formGroup]="entryForm" class="clr-form-horizontal">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="charttitle">Chart Title</label>
|
||||
<input id="chartparameter" type="text" formControlName="charttitle" class="clr-input" [(ngModel)]="gadgetsEditdata.charttitle" >
|
||||
<input id="chartparameter" type="text" formControlName="charttitle" class="clr-input"
|
||||
[(ngModel)]="gadgetsEditdata.charttitle">
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="clr-row">
|
||||
|
||||
|
||||
<!-- Compact Filter Configuration (shown only for Compact Filter components) -->
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName === 'Compact Filter'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="id">ID</label>
|
||||
<input id="datasource" type="text" formControlName="id" class="clr-input" [(ngModel)]="gadgetsEditdata.id">
|
||||
<h4>Compact Filter Configuration</h4>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="compactFilterConnection">Connection</label>
|
||||
<select id="compactFilterConnection" class="clr-select" [(ngModel)]="gadgetsEditdata.connection"
|
||||
(ngModelChange)="onCompactFilterConnectionChange($event)" [ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select a connection for this compact filter</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="compactFilterApiUrl">API URL</label>
|
||||
<div>
|
||||
<input type="text" id="compactFilterApiUrl" class="clr-input" [(ngModel)]="gadgetsEditdata.table"
|
||||
(ngModelChange)="onCompactFilterApiUrlChange($event)" [ngModelOptions]="{standalone: true}"
|
||||
placeholder="Enter API URL">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="loadAvailableKeys(gadgetsEditdata.table, gadgetsEditdata.connection)"
|
||||
[disabled]="!gadgetsEditdata.table">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-subtext">Enter the API URL to fetch data for this filter</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterKey">Filter Key</label>
|
||||
<select id="filterKey" class="clr-select" [(ngModel)]="gadgetsEditdata.filterKey"
|
||||
(ngModelChange)="onFilterKeyChange($event)" [ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Filter Key</option>
|
||||
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the field name to filter on</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterType">Filter Type</label>
|
||||
<select id="filterType" class="clr-select" [(ngModel)]="gadgetsEditdata.filterType"
|
||||
(ngModelChange)="onFilterTypeChange($event)" [ngModelOptions]="{standalone: true}">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="multiselect">Multi-Select</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="toggle">Toggle</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the type of filter control to display</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterLabel">Filter Label (Optional)</label>
|
||||
<input type="text" id="filterLabel" class="clr-input" [(ngModel)]="gadgetsEditdata.filterLabel"
|
||||
[ngModelOptions]="{standalone: true}" placeholder="Enter filter label">
|
||||
<div class="clr-subtext">Label to display for this filter in the UI (if not provided, filter key will be
|
||||
used)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 10px;"
|
||||
*ngIf="gadgetsEditdata.filterType === 'dropdown' || gadgetsEditdata.filterType === 'multiselect'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="filterOptions">Filter Options (comma separated)</label>
|
||||
<input type="text" id="filterOptions" class="clr-input" [(ngModel)]="filterOptionsString"
|
||||
[ngModelOptions]="{standalone: true}" placeholder="Option1,Option2,Option3">
|
||||
<div class="clr-subtext">Comma-separated list of options for dropdown/multiselect filters</div>
|
||||
<div class="clr-subtext" *ngIf="gadgetsEditdata.filterKey">
|
||||
<strong>Available values for "{{ gadgetsEditdata.filterKey }}":</strong> {{ filterOptionsString }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName !== 'Grid View' && gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<div class="clr-form-control" style="margin-top: 5px;margin-bottom: 10px;">
|
||||
<div class="clr-control-container">
|
||||
<!-- <div class="clr-checkbox-wrapper">
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName !== 'Compact Filter'"
|
||||
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
|
||||
<!-- Add Connection Selection Field -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="connection">Connection</label>
|
||||
<select id="connection" formControlName="connection" [(ngModel)]="gadgetsEditdata.connection"
|
||||
class="clr-select">
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select a SureConnect connection to use for this chart</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row"
|
||||
*ngIf="gadgetsEditdata?.fieldName !== 'Grid View' && gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<div class="clr-form-control" style="margin-top: 5px;margin-bottom: 10px;">
|
||||
<div class="clr-control-container">
|
||||
<!-- <div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="slices" formControlName="slices" [(ngModel)]="gadgetsEditdata.slices" class="clr-checkbox" />
|
||||
<label for="slices" class="clr-control-label">Show colors in gradient</label>
|
||||
</div> -->
|
||||
<!-- <div class="clr-checkbox-wrapper">
|
||||
<!-- <div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="chartcolor" formControlName="chartcolor" [(ngModel)]="gadgetsEditdata.chartcolor" class="clr-checkbox" />
|
||||
<label for="chartcolor" class="clr-control-label">Show colors in gradient</label>
|
||||
</div> -->
|
||||
<!-- <div class="clr-checkbox-wrapper">
|
||||
<!-- <div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="donut" formControlName="donut"[(ngModel)]="gadgetsEditdata.donut" class="clr-checkbox" />
|
||||
<label for="donut" class="clr-control-label">Show donut</label>
|
||||
</div> -->
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="chartlegend" formControlName="chartlegend" [(ngModel)]="gadgetsEditdata.chartlegend" class="clr-checkbox" />
|
||||
<label for="chartlegend" class="clr-control-label">Show Chart Legend</label>
|
||||
</div>
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="showlabel" formControlName="showlabel" [(ngModel)]="gadgetsEditdata.showlabel" class="clr-checkbox" />
|
||||
<label for="showlabel" class="clr-control-label">Show Chart Label</label>
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="chartlegend" formControlName="chartlegend"
|
||||
[(ngModel)]="gadgetsEditdata.chartlegend" class="clr-checkbox" />
|
||||
<label for="chartlegend" class="clr-control-label">Show Chart Legend</label>
|
||||
</div>
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="showlabel" formControlName="showlabel"
|
||||
[(ngModel)]="gadgetsEditdata.showlabel" class="clr-checkbox" />
|
||||
<label for="showlabel" class="clr-control-label">Show Chart Label</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="clr-row">
|
||||
<!-- <div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="datasource">Data Store</label>
|
||||
<select formControlName="datastore" [(ngModel)]="gadgetsEditdata.datastore" (change)="storename($event.target.value)">
|
||||
@@ -125,74 +242,580 @@
|
||||
</select>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="clr-row" >
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="table">Table Name</label>
|
||||
<div><input type="urk" id="table" formControlName="table" class="clr-input" [(ngModel)]="gadgetsEditdata.table" style="width:90%"> <span><button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="tablename(gadgetsEditdata.table)">
|
||||
<clr-icon shape="redo"></clr-icon> </button></span></div>
|
||||
<!-- <select id="table" formControlName="table" [(ngModel)]="gadgetsEditdata.table" (change)="tablename($event.target.value)">
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="table">Api Url</label>
|
||||
<div><input type="urk" id="table" formControlName="table" class="clr-input"
|
||||
[(ngModel)]="gadgetsEditdata.table" style="width:90%"> <span><button
|
||||
class="btn btn-icon btn-primary" style="margin: 0px;" (click)="callApi(gadgetsEditdata.table)">
|
||||
<clr-icon shape="redo"></clr-icon> </button></span></div>
|
||||
<!-- <select id="table" formControlName="table" [(ngModel)]="gadgetsEditdata.table" (change)="tablename($event.target.value)">
|
||||
<option value="null">choose Table</option>
|
||||
<option *ngFor="let data of TableData" [value]="data">{{data}}</option>
|
||||
</select> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName !== 'Grid View'&& gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="xAxis">X-Axis <span *ngIf="gadgetsEditdata?.fieldName === 'Bubble Chart' || gadgetsEditdata?.fieldName === 'Scatter Chart' ">(Numeric)</span></label>
|
||||
<!-- <input id="xAxis" type="text" formControlName="xAxis" class="clr-input" [(ngModel)]="gadgetsEditdata.xAxis"> -->
|
||||
<select id="xAxis" formControlName="xAxis" [(ngModel)]="gadgetsEditdata.xAxis">
|
||||
<option value="null">choose Column</option>
|
||||
<option *ngFor="let data of columnData" [value]="data">{{data}}</option>
|
||||
</select>
|
||||
<div class="clr-row"
|
||||
*ngIf="gadgetsEditdata?.fieldName !== 'Grid View'&& gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="xAxis">X-Axis <span
|
||||
*ngIf="gadgetsEditdata?.fieldName === 'Bubble Chart' || gadgetsEditdata?.fieldName === 'Scatter Chart' ">(Numeric)</span></label>
|
||||
<!-- <input id="xAxis" type="text" formControlName="xAxis" class="clr-input" [(ngModel)]="gadgetsEditdata.xAxis"> -->
|
||||
<select id="xAxis" formControlName="xAxis" [(ngModel)]="gadgetsEditdata.xAxis">
|
||||
<option value="null">choose Column</option>
|
||||
<option *ngFor="let data of columnData" [value]="data">{{data}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName !== 'Pie Chart' && gadgetsEditdata?.fieldName !== 'Polar Area Chart' && gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="yAxis" *ngIf="gadgetsEditdata?.fieldName === 'Grid View'; else yaxislable">Columns</label>
|
||||
<ng-template #yaxislable>
|
||||
<div class="clr-row"
|
||||
*ngIf="gadgetsEditdata?.fieldName !== 'Pie Chart' && gadgetsEditdata?.fieldName !== 'Polar Area Chart' && gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="yAxis" *ngIf="gadgetsEditdata?.fieldName === 'Grid View'; else yaxislable">Columns</label>
|
||||
<ng-template #yaxislable>
|
||||
<label for="yAxis">Y-Axis (Numeric)</label>
|
||||
</ng-template>
|
||||
|
||||
<!-- <input id="yAxis" type="text" formControlName="yAxis" class="clr-input" [(ngModel)]="gadgetsEditdata.yAxis"> -->
|
||||
<clr-combobox-container style="margin-top: 10px !important;">
|
||||
<clr-combobox id="yAxis" [(ngModel)]="selectedyAxis" formControlName="yAxis" clrMulti="true" required>
|
||||
<ng-container *clrOptionSelected="let selected;let i = alias">
|
||||
{{selected}}
|
||||
</ng-container>
|
||||
<clr-options>
|
||||
<clr-option *clrOptionItems="let state of columnData" [clrValue]="state">
|
||||
{{state}}
|
||||
</clr-option>
|
||||
</clr-options>
|
||||
</clr-combobox>
|
||||
</clr-combobox-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row"
|
||||
*ngIf="gadgetsEditdata?.fieldName == 'Pie Chart' || gadgetsEditdata?.fieldName == 'Polar Area Chart' || gadgetsEditdata?.fieldName == 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="yAxis">Y-Axis (Numeric)</label>
|
||||
</ng-template>
|
||||
|
||||
<!-- <input id="yAxis" type="text" formControlName="yAxis" class="clr-input" [(ngModel)]="gadgetsEditdata.yAxis"> -->
|
||||
<clr-combobox-container style="margin-top: 10px !important;">
|
||||
<clr-combobox id="yAxis" [(ngModel)]="selectedyAxis" formControlName="yAxis" clrMulti="true" required>
|
||||
<ng-container *clrOptionSelected="let selected;let i = alias">
|
||||
{{selected}}
|
||||
</ng-container>
|
||||
<clr-options>
|
||||
<clr-option *clrOptionItems="let state of columnData" [clrValue]="state">
|
||||
{{state}}
|
||||
</clr-option>
|
||||
</clr-options>
|
||||
</clr-combobox>
|
||||
</clr-combobox-container>
|
||||
<select id="yAxis" formControlName="yAxis" [(ngModel)]="gadgetsEditdata.yAxis">
|
||||
<option value="null">choose Column</option>
|
||||
<option *ngFor="let data of columnData" [value]="data">{{data}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName == 'Pie Chart' || gadgetsEditdata?.fieldName == 'Polar Area Chart' || gadgetsEditdata?.fieldName == 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="yAxis">Y-Axis (Numeric)</label>
|
||||
<select id="yAxis" formControlName="yAxis" [(ngModel)]="gadgetsEditdata.yAxis">
|
||||
<option value="null">choose Column</option>
|
||||
<option *ngFor="let data of columnData" [value]="data">{{data}}</option>
|
||||
</select>
|
||||
|
||||
<!-- Base API Filters -->
|
||||
<div class="clr-row" style="margin-top: 15px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<h5>Base API Filters</h5>
|
||||
<div class="clr-subtext">Configure filters for the main API (applied regardless of drilldown settings)</div>
|
||||
|
||||
<!-- Common Filter Toggle -->
|
||||
<div class="clr-form-control" style="margin-top: 10px;">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="commonFilterToggle" [(ngModel)]="gadgetsEditdata.commonFilterEnabled"
|
||||
(change)="onCommonFilterToggle()" [ngModelOptions]="{standalone: true}" class="clr-checkbox" />
|
||||
<label for="commonFilterToggle" class="clr-control-label">Use Common Filter</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Base Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()"
|
||||
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Base Filter Fields List -->
|
||||
<div *ngFor="let filter of gadgetsEditdata.baseFilters; let i = index"
|
||||
style="margin-bottom: 10px; padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: #f9f9f9;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>Filter {{i + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeBaseFilter(i)"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 8px;">
|
||||
<div class="clr-col-sm-4">
|
||||
<select [(ngModel)]="filter.field" (ngModelChange)="onBaseFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<option value="">Select Field</option>
|
||||
<!-- Base API filters should always use columnData, not drilldownColumnData -->
|
||||
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.baseFilters, i, columnData)"
|
||||
[value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-3">
|
||||
<select [(ngModel)]="filter.type" (ngModelChange)="onBaseFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="multiselect">Multi-Select</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="toggle">Toggle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
|
||||
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabled" />
|
||||
<div class="clr-subtext" *ngIf="filter.availableValues">
|
||||
Available: {{ filter.availableValues }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeBaseFilter(i)"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabled">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="clr-row">
|
||||
|
||||
<!-- Base Drilldown Configuration Section -->
|
||||
<div class="clr-row" style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
<div class="clr-col-sm-12">
|
||||
<h4>Base Drilldown Configuration</h4>
|
||||
<div class="clr-form-control">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="drilldownEnabled" formControlName="drilldownEnabled"
|
||||
[(ngModel)]="gadgetsEditdata.drilldownEnabled" class="clr-checkbox"
|
||||
(change)="gadgetsEditdata.drilldownEnabled ? null : resetDrilldownConfiguration()" />
|
||||
<label for="drilldownEnabled" class="clr-control-label">Enable Base Drilldown</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownApiUrl">Base Drilldown API URL</label>
|
||||
<div>
|
||||
<input type="text" id="drilldownApiUrl" formControlName="drilldownApiUrl" class="clr-input"
|
||||
[(ngModel)]="gadgetsEditdata.drilldownApiUrl" style="width:90%"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshBaseDrilldownColumns()"
|
||||
[disabled]="!gadgetsEditdata.drilldownApiUrl">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-subtext">Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g.,
|
||||
http://api.example.com/data/<country></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownXAxis">Base Drilldown X-Axis</label>
|
||||
<select id="drilldownXAxis" formControlName="drilldownXAxis" [(ngModel)]="gadgetsEditdata.drilldownXAxis"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select X-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use for X-axis in base drilldown view</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled &&
|
||||
gadgetsEditdata?.fieldName !== 'Pie Chart' &&
|
||||
gadgetsEditdata?.fieldName !== 'Polar Area Chart' &&
|
||||
gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownYAxis">Base Drilldown Y-Axis</label>
|
||||
<select id="drilldownYAxis" formControlName="drilldownYAxis" [(ngModel)]="gadgetsEditdata.drilldownYAxis"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Y-Axis Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use for Y-axis in base drilldown view</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base Drilldown Parameter Configuration -->
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="drilldownParameter">Base Drilldown Parameter</label>
|
||||
<select id="drilldownParameter" [(ngModel)]="gadgetsEditdata.drilldownParameter"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in base
|
||||
drilldown
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base Drilldown Filter Configuration -->
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled" style="margin-top: 15px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<h5>Base Drilldown Filters</h5>
|
||||
<div class="clr-subtext">Configure filters for the base drilldown level</div>
|
||||
|
||||
<!-- Common Filter Toggle for Base Drilldown -->
|
||||
<div class="clr-form-control" style="margin-top: 10px;">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" id="commonFilterToggleDrilldown"
|
||||
[(ngModel)]="gadgetsEditdata.commonFilterEnabledDrilldown"
|
||||
(change)="onCommonFilterToggleDrilldown()" [ngModelOptions]="{standalone: true}"
|
||||
class="clr-checkbox" />
|
||||
<label for="commonFilterToggleDrilldown" class="clr-control-label">Use Common Filter</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Drilldown Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()"
|
||||
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Drilldown Filter Fields List -->
|
||||
<div *ngFor="let filter of gadgetsEditdata.drilldownFilters; let i = index"
|
||||
style="margin-bottom: 10px; padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: #f9f9f9;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>Filter {{i + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownFilter(i)"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 8px;">
|
||||
<div class="clr-col-sm-4">
|
||||
<select [(ngModel)]="filter.field" (ngModelChange)="onDrilldownFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<option value="">Select Field</option>
|
||||
<option
|
||||
*ngFor="let column of getAvailableFields(gadgetsEditdata.drilldownFilters, i, drilldownColumnData)"
|
||||
[value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-3">
|
||||
<select [(ngModel)]="filter.type" (ngModelChange)="onDrilldownFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="multiselect">Multi-Select</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="toggle">Toggle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
|
||||
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
|
||||
<div class="clr-subtext" *ngIf="filter.availableValues">
|
||||
Available: {{ filter.availableValues }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownFilter(i)"
|
||||
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Multi-Layer Drilldown Configurations -->
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled"
|
||||
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
<div class="clr-col-sm-12">
|
||||
<h4>Multi-Layer Drilldown Configurations</h4>
|
||||
<button class="btn btn-sm btn-primary" (click)="addDrilldownLayer()">
|
||||
<clr-icon shape="plus"></clr-icon> Add Drilldown Layer
|
||||
</button>
|
||||
<div class="clr-subtext">Add additional drilldown layers for multi-level navigation</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Drilldown Layers -->
|
||||
<div class="clr-row" *ngFor="let layer of gadgetsEditdata.drilldownLayers; let i = index">
|
||||
<div class="clr-col-sm-12"
|
||||
style="margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h5>Drilldown Layer {{i + 1}}</h5>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownLayer(i)">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" class="clr-checkbox"
|
||||
[ngModelOptions]="{standalone: true}" />
|
||||
<label [for]="'layerEnabled' + i" class="clr-control-label">Enable Layer {{i + 1}} Drilldown</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label [for]="'layerApiUrl' + i">Layer {{i + 1}} API URL</label>
|
||||
<div>
|
||||
<input type="text" [id]="'layerApiUrl' + i" class="clr-input" [(ngModel)]="layer.apiUrl"
|
||||
style="width:90%" [ngModelOptions]="{standalone: true}">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;"
|
||||
(click)="refreshDrilldownLayerColumns(i)" [disabled]="!layer.apiUrl">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-subtext">Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for
|
||||
parameters, e.g., http://api.example.com/data/<state></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label [for]="'layerXAxis' + i">Layer {{i + 1}} X-Axis</label>
|
||||
<select [id]="'layerXAxis' + i" [(ngModel)]="layer.xAxis" [ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select X-Axis Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use for X-axis in layer {{i + 1}} drilldown view</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName !== 'Pie Chart' &&
|
||||
gadgetsEditdata?.fieldName !== 'Polar Area Chart' &&
|
||||
gadgetsEditdata?.fieldName !== 'To Do Chart'">
|
||||
<div class="clr-col-sm-12">
|
||||
<label [for]="'layerYAxis' + i">Layer {{i + 1}} Y-Axis</label>
|
||||
<select [id]="'layerYAxis' + i" [(ngModel)]="layer.yAxis" [ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Y-Axis Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use for Y-axis in layer {{i + 1}} drilldown view</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameter Selection for Drilldown Layer -->
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label [for]="'layerParameter' + i">Layer {{i + 1}} Parameter</label>
|
||||
<select [id]="'layerParameter' + i" [(ngModel)]="layer.parameter" [ngModelOptions]="{standalone: true}">
|
||||
<option value="">Select Parameter Column</option>
|
||||
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in layer {{i
|
||||
+
|
||||
1}} drilldown</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filter Configuration -->
|
||||
<div class="clr-row" style="margin-top: 15px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<h5>Layer {{i + 1}} Filters</h5>
|
||||
<div class="clr-subtext">Configure filters for this drilldown layer</div>
|
||||
|
||||
<!-- Common Filter Toggle for Layer -->
|
||||
<div class="clr-form-control" style="margin-top: 10px;">
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input type="checkbox" [id]="'commonFilterToggleLayer' + i"
|
||||
[(ngModel)]="layer.commonFilterEnabled" (change)="onCommonFilterToggleLayer(i)"
|
||||
[ngModelOptions]="{standalone: true}" class="clr-checkbox" />
|
||||
<label [for]="'commonFilterToggleLayer' + i" class="clr-control-label">Use Common Filter</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Layer Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)"
|
||||
style="margin-top: 10px; margin-bottom: 10px;" [disabled]="layer.commonFilterEnabled">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Layer Filter Fields List -->
|
||||
<div *ngFor="let filter of layer.filters; let j = index"
|
||||
style="margin-bottom: 10px; padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: #f9f9f9;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>Filter {{j + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeLayerFilter(i, j)"
|
||||
[disabled]="layer.commonFilterEnabled">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 8px;">
|
||||
<div class="clr-col-sm-4">
|
||||
<select [(ngModel)]="filter.field" (ngModelChange)="onLayerFilterFieldChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="layer.commonFilterEnabled">
|
||||
<option value="">Select Field</option>
|
||||
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])"
|
||||
[value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-3">
|
||||
<select [(ngModel)]="filter.type" (ngModelChange)="onLayerFilterTypeChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
|
||||
[disabled]="layer.commonFilterEnabled">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="multiselect">Multi-Select</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="toggle">Toggle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
|
||||
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Option1,Option2,Option3" [disabled]="layer.commonFilterEnabled" />
|
||||
<div class="clr-subtext" *ngIf="filter.availableValues">
|
||||
Available: {{ filter.availableValues }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeLayerFilter(i, j)"
|
||||
[disabled]="layer.commonFilterEnabled">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="chartparameter">API parameter</label>
|
||||
<input id="chartparameter" type="text" formControlName="chartparameter" class="clr-input" [(ngModel)]="gadgetsEditdata.chartparameter">
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="modeledit = false">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="onSubmit(modelid)" >save</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="modeledit = false">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="applyChanges(modelid)">Apply</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="onSubmit(modelid)">Save</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
|
||||
</clr-modal>
|
||||
|
||||
<!-- Common Filter Modal -->
|
||||
<clr-modal [(clrModalOpen)]="commonFilterModalOpen" [clrModalStaticBackdrop]="true" clrModalSize="lg">
|
||||
<h3 class="modal-title">Configure Common Filter</h3>
|
||||
<div class="modal-body">
|
||||
<form [formGroup]="commonFilterForm" class="clr-form-horizontal">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="commonFilterConnection">Connection</label>
|
||||
<select id="commonFilterConnection" formControlName="connection" [(ngModel)]="commonFilterData.connection"
|
||||
class="clr-select">
|
||||
<option value="">Select Connection</option>
|
||||
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||
{{conn.connection_name || conn.id}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-sm-12">
|
||||
<label for="commonFilterApiUrl">API URL</label>
|
||||
<div>
|
||||
<input type="text" id="commonFilterApiUrl" formControlName="apiUrl" class="clr-input"
|
||||
[(ngModel)]="commonFilterData.apiUrl" style="width:90%">
|
||||
<span>
|
||||
<button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshCommonFilterColumns()"
|
||||
[disabled]="!commonFilterData.apiUrl">
|
||||
<clr-icon shape="redo"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Common Filter Fields List -->
|
||||
<div class="clr-row" style="margin-top: 15px;">
|
||||
<div class="clr-col-sm-12">
|
||||
<h5>Common Filters</h5>
|
||||
|
||||
<!-- Add Common Filter Button -->
|
||||
<button class="btn btn-sm btn-primary" (click)="addCommonFilter()"
|
||||
style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<clr-icon shape="plus"></clr-icon> Add Filter
|
||||
</button>
|
||||
|
||||
<!-- Common Filter Fields List -->
|
||||
<div *ngFor="let filter of commonFilterData.filters; let i = index"
|
||||
style="margin-bottom: 10px; padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: #f9f9f9;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>Filter {{i + 1}}</span>
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeCommonFilter(i)">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clr-row" style="margin-top: 8px;">
|
||||
<div class="clr-col-sm-5">
|
||||
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select">
|
||||
<option value="">Select Field</option>
|
||||
<option *ngFor="let column of commonFilterColumnData" [value]="column">{{column}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-5">
|
||||
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
|
||||
placeholder="Filter Value" />
|
||||
</div>
|
||||
|
||||
<div class="clr-col-sm-2">
|
||||
<button class="btn btn-icon btn-danger btn-sm" (click)="removeCommonFilter(i)">
|
||||
<clr-icon shape="trash"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="commonFilterModalOpen = false">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="saveCommonFilter()">Save</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,283 @@
|
||||
# Drilldown Configuration Implementation
|
||||
|
||||
## Overview
|
||||
This document describes the drilldown configuration implementation applied to all chart components in the dashboard system. The implementation provides multi-layer drilldown functionality with parameter passing capabilities, allowing users to navigate through hierarchical data structures.
|
||||
|
||||
## Components with Drilldown Support
|
||||
|
||||
The following chart components have drilldown functionality implemented:
|
||||
|
||||
1. Bar Chart (`bar-chart`)
|
||||
2. Line Chart (`line-chart`)
|
||||
3. Pie Chart (`pie-chart`)
|
||||
4. Bubble Chart (`bubble-chart`)
|
||||
5. Doughnut Chart (`doughnut-chart`)
|
||||
6. Polar Chart (`polar-chart`)
|
||||
7. Radar Chart (`radar-chart`)
|
||||
8. Scatter Chart (`scatter-chart`)
|
||||
9. Financial Chart (`financial-chart`)
|
||||
10. Dynamic Chart (`dynamic-chart`)
|
||||
|
||||
## Drilldown Configuration Properties
|
||||
|
||||
Each chart component includes the following drilldown configuration inputs:
|
||||
|
||||
```typescript
|
||||
// Drilldown configuration inputs
|
||||
@Input() drilldownEnabled: boolean = false;
|
||||
@Input() drilldownApiUrl: string;
|
||||
@Input() drilldownXAxis: string;
|
||||
@Input() drilldownYAxis: string;
|
||||
@Input() drilldownParameter: string;
|
||||
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = [];
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. State Management
|
||||
|
||||
Each component maintains drilldown state through the following properties:
|
||||
|
||||
```typescript
|
||||
// Multi-layer drilldown state tracking
|
||||
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||
|
||||
// Original data storage for navigation
|
||||
originalChartLabels: string[] = []; // Stores original labels
|
||||
originalChartData: any[] = []; // Stores original data
|
||||
```
|
||||
|
||||
### 2. Core Methods
|
||||
|
||||
#### fetchDrilldownData()
|
||||
Fetches data for the current drilldown level based on configuration:
|
||||
|
||||
```typescript
|
||||
fetchDrilldownData(): void {
|
||||
// Determine drilldown configuration based on current level
|
||||
let drilldownConfig;
|
||||
if (this.currentDrilldownLevel === 1) {
|
||||
// Base drilldown level
|
||||
drilldownConfig = {
|
||||
apiUrl: this.drilldownApiUrl,
|
||||
xAxis: this.drilldownXAxis,
|
||||
yAxis: this.drilldownYAxis,
|
||||
parameter: this.drilldownParameter
|
||||
};
|
||||
} else {
|
||||
// Multi-layer drilldown level
|
||||
const layerIndex = this.currentDrilldownLevel - 2;
|
||||
if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
|
||||
drilldownConfig = this.drilldownLayers[layerIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// Get parameter value from drilldown stack
|
||||
let parameterValue = '';
|
||||
if (this.drilldownStack.length > 0) {
|
||||
const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
|
||||
parameterValue = lastEntry.clickedValue || '';
|
||||
}
|
||||
|
||||
// Replace parameter placeholders in API URL
|
||||
let actualApiUrl = drilldownConfig.apiUrl;
|
||||
if (parameterValue) {
|
||||
const encodedValue = encodeURIComponent(parameterValue);
|
||||
actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
|
||||
}
|
||||
|
||||
// Fetch data from service
|
||||
this.dashboardService.getChartData(
|
||||
actualApiUrl,
|
||||
chartType,
|
||||
drilldownConfig.xAxis,
|
||||
drilldownConfig.yAxis,
|
||||
this.connection,
|
||||
drilldownConfig.parameter,
|
||||
parameterValue
|
||||
).subscribe(...);
|
||||
}
|
||||
```
|
||||
|
||||
#### chartClicked()
|
||||
Handles chart click events to initiate drilldown navigation:
|
||||
|
||||
```typescript
|
||||
public chartClicked(e: any): void {
|
||||
// Check if drilldown is enabled and we have a valid click event
|
||||
if (this.drilldownEnabled && e.active && e.active.length > 0) {
|
||||
// Get clicked element details
|
||||
const clickedIndex = e.active[0].index;
|
||||
const clickedLabel = this.chartLabels[clickedIndex];
|
||||
|
||||
// Store original data if we're at base level
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
this.originalChartLabels = [...this.chartLabels];
|
||||
this.originalChartData = [...this.chartData];
|
||||
}
|
||||
|
||||
// Determine next drilldown level
|
||||
const nextDrilldownLevel = this.currentDrilldownLevel + 1;
|
||||
|
||||
// Check if there's a drilldown configuration for this level
|
||||
let hasDrilldownConfig = false;
|
||||
let drilldownConfig;
|
||||
|
||||
if (nextDrilldownLevel === 1) {
|
||||
// Base drilldown level
|
||||
drilldownConfig = {
|
||||
apiUrl: this.drilldownApiUrl,
|
||||
xAxis: this.drilldownXAxis,
|
||||
yAxis: this.drilldownYAxis,
|
||||
parameter: this.drilldownParameter
|
||||
};
|
||||
hasDrilldownConfig = !!this.drilldownApiUrl && !!this.drilldownXAxis && !!this.drilldownYAxis;
|
||||
} else {
|
||||
// Multi-layer drilldown level
|
||||
const layerIndex = nextDrilldownLevel - 2;
|
||||
if (layerIndex < this.drilldownLayers.length) {
|
||||
drilldownConfig = this.drilldownLayers[layerIndex];
|
||||
hasDrilldownConfig = drilldownConfig.enabled &&
|
||||
!!drilldownConfig.apiUrl &&
|
||||
!!drilldownConfig.xAxis &&
|
||||
!!drilldownConfig.yAxis;
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed with drilldown if configuration exists
|
||||
if (hasDrilldownConfig) {
|
||||
// Add click to drilldown stack
|
||||
const stackEntry = {
|
||||
level: nextDrilldownLevel,
|
||||
clickedIndex: clickedIndex,
|
||||
clickedLabel: clickedLabel,
|
||||
clickedValue: clickedLabel
|
||||
};
|
||||
|
||||
this.drilldownStack.push(stackEntry);
|
||||
this.currentDrilldownLevel = nextDrilldownLevel;
|
||||
|
||||
// Fetch drilldown data
|
||||
this.fetchDrilldownData();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### navigateBack()
|
||||
Navigates back to the previous drilldown level:
|
||||
|
||||
```typescript
|
||||
navigateBack(): void {
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Remove last entry from stack
|
||||
this.drilldownStack.pop();
|
||||
this.currentDrilldownLevel = this.drilldownStack.length;
|
||||
|
||||
if (this.drilldownStack.length > 0) {
|
||||
// Fetch data for previous level
|
||||
this.fetchDrilldownData();
|
||||
} else {
|
||||
// Back to base level
|
||||
this.resetToOriginalData();
|
||||
}
|
||||
} else {
|
||||
// Already at base level
|
||||
this.resetToOriginalData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### resetToOriginalData()
|
||||
Resets the chart to its original data:
|
||||
|
||||
```typescript
|
||||
resetToOriginalData(): void {
|
||||
this.currentDrilldownLevel = 0;
|
||||
this.drilldownStack = [];
|
||||
|
||||
if (this.originalChartLabels.length > 0) {
|
||||
this.chartLabels = [...this.originalChartLabels];
|
||||
}
|
||||
if (this.originalChartData.length > 0) {
|
||||
this.chartData = [...this.originalChartData];
|
||||
}
|
||||
|
||||
// Re-fetch original data
|
||||
this.fetchChartData();
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Layer Drilldown Support
|
||||
|
||||
The implementation supports multiple drilldown layers through the `drilldownLayers` array. Each layer can have its own configuration:
|
||||
|
||||
```typescript
|
||||
drilldownLayers: [
|
||||
{
|
||||
enabled: true,
|
||||
apiUrl: "second-level-endpoint/<parameter>",
|
||||
xAxis: "column1",
|
||||
yAxis: "column2",
|
||||
parameter: "selectedColumn"
|
||||
},
|
||||
{
|
||||
enabled: true,
|
||||
apiUrl: "third-level-endpoint/<parameter>",
|
||||
xAxis: "column3",
|
||||
yAxis: "column4",
|
||||
parameter: "selectedColumn"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Parameter Passing
|
||||
|
||||
The drilldown implementation supports parameter passing by replacing placeholders in the API URL:
|
||||
|
||||
1. URL templates use angle brackets for parameter placeholders: `endpoint/<parameter>`
|
||||
2. When navigating, the clicked value replaces the placeholder
|
||||
3. Parameters are properly encoded using `encodeURIComponent`
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **Initial Load**: Chart loads with base data using `fetchChartData()`
|
||||
2. **Drilldown Initiation**: User clicks on chart element, triggering `chartClicked()`
|
||||
3. **Data Fetch**: New data is fetched using `fetchDrilldownData()` with parameter replacement
|
||||
4. **Navigation**: User can navigate back using `navigateBack()` or reset using `resetToOriginalData()`
|
||||
5. **State Management**: All navigation is tracked in `drilldownStack` with level management
|
||||
|
||||
## Error Handling
|
||||
|
||||
The implementation includes error handling for:
|
||||
|
||||
1. Missing drilldown configuration
|
||||
2. API call failures
|
||||
3. Invalid data structures
|
||||
4. Null responses from backend
|
||||
|
||||
In case of errors, the chart maintains its current data and displays appropriate warnings in the console.
|
||||
|
||||
## UI Integration
|
||||
|
||||
Components with drilldown support should include UI elements for:
|
||||
|
||||
1. **Back Button**: To navigate to previous drilldown level
|
||||
2. **Reset Button**: To return to original data
|
||||
3. **Navigation Indicators**: To show current drilldown level
|
||||
|
||||
Example HTML structure:
|
||||
|
||||
```html
|
||||
<div *ngIf="drilldownEnabled && currentDrilldownLevel > 0" class="drilldown-controls">
|
||||
<button (click)="navigateBack()" class="btn btn-secondary">
|
||||
← Back to Level {{ currentDrilldownLevel - 1 }}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" class="btn btn-outline">
|
||||
↺ Reset to Original
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
Binary file not shown.
@@ -0,0 +1,142 @@
|
||||
# Bar Chart Filter Configuration
|
||||
|
||||
## Overview
|
||||
This document describes the filter configuration implementation for the Bar Chart component. The implementation provides multiple filter capabilities allowing users to dynamically filter chart data.
|
||||
|
||||
## Filter Configuration Properties
|
||||
|
||||
The Bar Chart component includes the following filter configuration inputs:
|
||||
|
||||
```typescript
|
||||
// Filter configuration inputs
|
||||
@Input() filterFields: any[] = []; // Array of filter field configurations
|
||||
@Input() enableFilters: boolean = false; // Enable/disable filter functionality
|
||||
```
|
||||
|
||||
## Filter Field Configuration
|
||||
|
||||
Each filter field in the `filterFields` array should have the following structure:
|
||||
|
||||
```typescript
|
||||
{
|
||||
field: string, // The field name to filter on
|
||||
label?: string, // Optional label to display (defaults to field name)
|
||||
type: string, // Filter type: 'text', 'dropdown', 'number-range'
|
||||
options?: any[] // Options for dropdown filters (optional)
|
||||
}
|
||||
```
|
||||
|
||||
### Filter Types
|
||||
|
||||
1. **Text Filter** (`type: 'text'`)
|
||||
- Simple text input field
|
||||
- Allows users to enter text to filter data
|
||||
|
||||
2. **Dropdown Filter** (`type: 'dropdown'`)
|
||||
- Dropdown selection field
|
||||
- Requires `options` array with values
|
||||
|
||||
3. **Number Range Filter** (`type: 'number-range'`)
|
||||
- Two number input fields (min and max)
|
||||
- Allows users to filter by numeric ranges
|
||||
|
||||
## Programmatic Filter Methods
|
||||
|
||||
The Bar Chart component provides several methods for programmatic filter control:
|
||||
|
||||
### setFilterOptions(field: string, options: any[])
|
||||
Sets filter options for a dropdown filter field.
|
||||
|
||||
### getFilterValues(): any
|
||||
Returns current filter values as an object.
|
||||
|
||||
### setFilterValues(filterValues: any)
|
||||
Sets filter values programmatically and refreshes the chart.
|
||||
|
||||
### updateFilter(field: string, value: string)
|
||||
Updates a specific filter value and refreshes the chart.
|
||||
|
||||
### clearFilters()
|
||||
Clears all filter values and refreshes the chart.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Enable Filters
|
||||
```html
|
||||
<app-bar-chart
|
||||
[enableFilters]="true"
|
||||
[filterFields]="[
|
||||
{ field: 'category', label: 'Category', type: 'dropdown', options: ['A', 'B', 'C'] },
|
||||
{ field: 'name', label: 'Name', type: 'text' },
|
||||
{ field: 'amount', label: 'Amount', type: 'number-range' }
|
||||
]"
|
||||
[table]="'sales_data'"
|
||||
[xAxis]="'product'"
|
||||
[yAxis]="'amount'">
|
||||
</app-bar-chart>
|
||||
```
|
||||
|
||||
### Programmatic Filter Control
|
||||
```typescript
|
||||
// Set filter options
|
||||
chartComponent.setFilterOptions('category', [
|
||||
{ value: 'A', label: 'Category A' },
|
||||
{ value: 'B', label: 'Category B' },
|
||||
{ value: 'C', label: 'Category C' }
|
||||
]);
|
||||
|
||||
// Set filter values
|
||||
chartComponent.setFilterValues({
|
||||
'category': 'A',
|
||||
'name': 'Product 1'
|
||||
});
|
||||
|
||||
// Get current filter values
|
||||
const currentFilters = chartComponent.getFilterValues();
|
||||
console.log(currentFilters);
|
||||
|
||||
// Clear all filters
|
||||
chartComponent.clearFilters();
|
||||
```
|
||||
|
||||
### Filter Options Format
|
||||
For dropdown filters, options can be provided in multiple formats:
|
||||
|
||||
1. Simple array of strings:
|
||||
```javascript
|
||||
options: ['Option 1', 'Option 2', 'Option 3']
|
||||
```
|
||||
|
||||
2. Array of objects with value/label:
|
||||
```javascript
|
||||
options: [
|
||||
{ value: 'opt1', label: 'Option 1' },
|
||||
{ value: 'opt2', label: 'Option 2' },
|
||||
{ value: 'opt3', label: 'Option 3' }
|
||||
]
|
||||
```
|
||||
|
||||
## Backend Integration
|
||||
|
||||
Filters are passed to the backend API as query parameters with the prefix `filter_`. For example:
|
||||
- Text filter: `filter_name=John`
|
||||
- Dropdown filter: `filter_category=A`
|
||||
- Number range filter: `filter_amount_min=100&filter_amount_max=500`
|
||||
|
||||
The backend should implement logic to filter data based on these parameters.
|
||||
|
||||
## UI Features
|
||||
|
||||
1. **Filter Controls Panel**: Appears at the top of the chart when filters are enabled
|
||||
2. **Clear Filters Button**: Resets all filters to their default state
|
||||
3. **Responsive Layout**: Filter controls automatically wrap on smaller screens
|
||||
4. **Real-time Updates**: Chart updates immediately when filter values change
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The filter implementation includes:
|
||||
- Two-way data binding for filter values
|
||||
- Dynamic filter control generation based on configuration
|
||||
- Filter parameter building for API calls
|
||||
- Filter state management
|
||||
- Integration with existing drilldown functionality
|
||||
@@ -0,0 +1,116 @@
|
||||
# Bar Chart Filter Implementation Summary
|
||||
|
||||
## Overview
|
||||
This document summarizes the implementation of filter functionality for the Bar Chart component, allowing users to dynamically filter chart data using multiple filter types.
|
||||
|
||||
## Files Modified
|
||||
|
||||
### 1. Bar Chart Component (bar-chart.component.ts)
|
||||
- Added filter configuration inputs: `filterFields` and `enableFilters`
|
||||
- Added filter state properties: `activeFilters` and `filterOptions`
|
||||
- Implemented filter initialization method
|
||||
- Added methods for programmatic filter control:
|
||||
- `setFilterOptions()`
|
||||
- `getFilterValues()`
|
||||
- `setFilterValues()`
|
||||
- `updateFilter()`
|
||||
- `clearFilters()`
|
||||
- Implemented `buildFilterParameters()` method to construct API query parameters
|
||||
- Updated `fetchChartData()` and `fetchDrilldownData()` to include filter parameters
|
||||
- Integrated filter functionality with existing drilldown implementation
|
||||
|
||||
### 2. Bar Chart Template (bar-chart.component.html)
|
||||
- Added filter controls panel that appears when filters are enabled
|
||||
- Implemented dynamic filter control generation based on `filterFields` configuration
|
||||
- Added support for three filter types:
|
||||
- Text input filters
|
||||
- Dropdown filters
|
||||
- Number range filters (min/max)
|
||||
- Added "Clear Filters" button
|
||||
- Implemented responsive layout for filter controls
|
||||
|
||||
### 3. Dashboard Service (dashboard3.service.ts)
|
||||
- No changes needed as filter parameters are passed as query string parameters
|
||||
|
||||
## New Features
|
||||
|
||||
### Filter Configuration
|
||||
- Enable/disable filter functionality with `enableFilters` input
|
||||
- Configure filter fields with `filterFields` input array
|
||||
- Support for multiple filter types: text, dropdown, number range
|
||||
|
||||
### UI Components
|
||||
- Filter controls panel at the top of the chart
|
||||
- Dynamic filter control generation
|
||||
- Responsive layout that adapts to screen size
|
||||
- Clear filters button
|
||||
|
||||
### Programmatic Control
|
||||
- Methods to set/get filter values programmatically
|
||||
- Method to set filter options for dropdown filters
|
||||
- Method to clear all filters
|
||||
|
||||
### Backend Integration
|
||||
- Filter parameters passed as query string parameters with `filter_` prefix
|
||||
- Support for range filters with `_min` and `_max` suffixes
|
||||
- Compatible with existing drilldown functionality
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Implementation
|
||||
```html
|
||||
<app-bar-chart
|
||||
[enableFilters]="true"
|
||||
[filterFields]="[
|
||||
{ field: 'category', label: 'Category', type: 'dropdown', options: ['A', 'B', 'C'] },
|
||||
{ field: 'name', label: 'Name', type: 'text' },
|
||||
{ field: 'amount', label: 'Amount', type: 'number-range' }
|
||||
]"
|
||||
[table]="'sales_data'"
|
||||
[xAxis]="'product'"
|
||||
[yAxis]="'amount'">
|
||||
</app-bar-chart>
|
||||
```
|
||||
|
||||
### Programmatic Control
|
||||
```typescript
|
||||
// Set filter options
|
||||
chartComponent.setFilterOptions('category', [
|
||||
{ value: 'A', label: 'Category A' },
|
||||
{ value: 'B', label: 'Category B' }
|
||||
]);
|
||||
|
||||
// Set filter values
|
||||
chartComponent.setFilterValues({
|
||||
'category': 'A',
|
||||
'name': 'Product 1'
|
||||
});
|
||||
|
||||
// Clear all filters
|
||||
chartComponent.clearFilters();
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
Filters are passed to the backend as query parameters:
|
||||
- Text filter: `filter_name=John`
|
||||
- Dropdown filter: `filter_category=A`
|
||||
- Number range filter: `filter_amount_min=100&filter_amount_max=500`
|
||||
|
||||
The backend should implement logic to filter data based on these parameters.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Fully compatible with existing drilldown functionality
|
||||
- Works with all existing chart configuration options
|
||||
- No breaking changes to existing API
|
||||
- Backward compatible - filters are disabled by default
|
||||
|
||||
## Testing
|
||||
|
||||
The implementation has been tested with:
|
||||
- All filter types (text, dropdown, number range)
|
||||
- Multiple simultaneous filters
|
||||
- Integration with drilldown functionality
|
||||
- Programmatic filter control
|
||||
- Responsive layout on different screen sizes
|
||||
@@ -0,0 +1,86 @@
|
||||
# Bar Chart Filter Usage Example
|
||||
|
||||
## Overview
|
||||
This document provides examples of how to use the new filter functionality in the Bar Chart component.
|
||||
|
||||
## Basic Filter Example
|
||||
|
||||
```html
|
||||
<app-bar-chart
|
||||
[enableFilters]="true"
|
||||
[filterFields]="[
|
||||
{ field: 'region', label: 'Region', type: 'dropdown', options: ['North', 'South', 'East', 'West'] },
|
||||
{ field: 'product', label: 'Product', type: 'text' },
|
||||
{ field: 'sales', label: 'Sales Amount', type: 'number-range' }
|
||||
]"
|
||||
[table]="'sales_data'"
|
||||
[xAxis]="'product'"
|
||||
[yAxis]="'amount'"
|
||||
[charttitle]="'Sales by Product'">
|
||||
</app-bar-chart>
|
||||
```
|
||||
|
||||
## Filter Types
|
||||
|
||||
### 1. Text Filter
|
||||
```javascript
|
||||
{ field: 'name', label: 'Name', type: 'text' }
|
||||
```
|
||||
|
||||
### 2. Dropdown Filter
|
||||
```javascript
|
||||
{ field: 'category', label: 'Category', type: 'dropdown', options: ['A', 'B', 'C'] }
|
||||
```
|
||||
|
||||
### 3. Number Range Filter
|
||||
```javascript
|
||||
{ field: 'amount', label: 'Amount', type: 'number-range' }
|
||||
```
|
||||
|
||||
## Advanced Example with Drilldown
|
||||
|
||||
```html
|
||||
<app-bar-chart
|
||||
[enableFilters]="true"
|
||||
[filterFields]="[
|
||||
{ field: 'year', label: 'Year', type: 'dropdown', options: ['2020', '2021', '2022', '2023'] },
|
||||
{ field: 'region', label: 'Region', type: 'text' }
|
||||
]"
|
||||
[table]="'sales_summary'"
|
||||
[xAxis]="'category'"
|
||||
[yAxis]="'revenue'"
|
||||
[charttitle]="'Revenue by Category'"
|
||||
[drilldownEnabled]="true"
|
||||
[drilldownApiUrl]="'sales_detail/<category>'"
|
||||
[drilldownXAxis]="'product'"
|
||||
[drilldownYAxis]="'amount'"
|
||||
[drilldownParameter]="'category'">
|
||||
</app-bar-chart>
|
||||
```
|
||||
|
||||
## Backend Integration
|
||||
|
||||
Filters are passed to the backend API as query parameters:
|
||||
- Text filter: `filter_name=John`
|
||||
- Dropdown filter: `filter_category=A`
|
||||
- Number range filter: `filter_amount_min=100&filter_amount_max=500`
|
||||
|
||||
The backend should implement logic to filter data based on these parameters.
|
||||
|
||||
## Filter Options Format
|
||||
|
||||
For dropdown filters, options can be provided in multiple formats:
|
||||
|
||||
1. Simple array:
|
||||
```javascript
|
||||
options: ['Option 1', 'Option 2', 'Option 3']
|
||||
```
|
||||
|
||||
2. Array of objects:
|
||||
```javascript
|
||||
options: [
|
||||
{ value: 'opt1', label: 'Option 1' },
|
||||
{ value: 'opt2', label: 'Option 2' },
|
||||
{ value: 'opt3', label: 'Option 3' }
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-chart-example',
|
||||
template: `
|
||||
<div style="padding: 20px;">
|
||||
<h2>Bar Chart with Filter Example</h2>
|
||||
|
||||
<!-- Example 1: Bar chart with text and dropdown filters -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<h3>Example 1: Sales Data with Filters</h3>
|
||||
<app-bar-chart
|
||||
[enableFilters]="true"
|
||||
[filterFields]="[
|
||||
{ field: 'region', label: 'Region', type: 'dropdown', options: ['North', 'South', 'East', 'West'] },
|
||||
{ field: 'product', label: 'Product', type: 'text' },
|
||||
{ field: 'sales', label: 'Sales Amount', type: 'number-range' }
|
||||
]"
|
||||
[table]="'sales_data'"
|
||||
[xAxis]="'product'"
|
||||
[yAxis]="'amount'"
|
||||
[charttitle]="'Sales by Product'"
|
||||
[chartlegend]="true"
|
||||
[showlabel]="true">
|
||||
</app-bar-chart>
|
||||
</div>
|
||||
|
||||
<!-- Example 2: Bar chart with drilldown and filters -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<h3>Example 2: Sales Data with Drilldown and Filters</h3>
|
||||
<app-bar-chart
|
||||
[enableFilters]="true"
|
||||
[filterFields]="[
|
||||
{ field: 'year', label: 'Year', type: 'dropdown', options: ['2020', '2021', '2022', '2023'] },
|
||||
{ field: 'category', label: 'Category', type: 'text' }
|
||||
]"
|
||||
[table]="'sales_summary'"
|
||||
[xAxis]="'category'"
|
||||
[yAxis]="'revenue'"
|
||||
[charttitle]="'Revenue by Category'"
|
||||
[drilldownEnabled]="true"
|
||||
[drilldownApiUrl]="'sales_detail/<category>'"
|
||||
[drilldownXAxis]="'product'"
|
||||
[drilldownYAxis]="'amount'"
|
||||
[drilldownParameter]="'category'">
|
||||
</app-bar-chart>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class BarChartExampleComponent {
|
||||
constructor() { }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-chart-test',
|
||||
template: `
|
||||
<div style="padding: 20px;">
|
||||
<h2>Bar Chart Filter Test</h2>
|
||||
|
||||
<app-bar-chart
|
||||
[enableFilters]="true"
|
||||
[filterFields]="[
|
||||
{ field: 'region', label: 'Region', type: 'dropdown', options: ['North', 'South', 'East', 'West'] },
|
||||
{ field: 'product', label: 'Product', type: 'text' },
|
||||
{ field: 'sales', label: 'Sales Amount', type: 'number-range' }
|
||||
]"
|
||||
[table]="'sales_data'"
|
||||
[xAxis]="'product'"
|
||||
[yAxis]="'amount'"
|
||||
[charttitle]="'Sales by Product'"
|
||||
[chartlegend]="true"
|
||||
[showlabel]="true">
|
||||
</app-bar-chart>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class BarChartTestComponent {
|
||||
constructor() { }
|
||||
}
|
||||
@@ -1,9 +1,334 @@
|
||||
<div style="display: block">
|
||||
<canvas baseChart
|
||||
[datasets]="barChartData"
|
||||
[labels]="barChartLabels"
|
||||
[type]="barChartType"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="chart-header">
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Bar Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
|
||||
<div *ngIf="noDataAvailable" class="no-data-message">
|
||||
No data available
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="!noDataAvailable" class="chart-display">
|
||||
<canvas baseChart
|
||||
[datasets]="barChartData"
|
||||
[labels]="barChartLabels"
|
||||
[type]="barChartType"
|
||||
[options]="barChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
</div>
|
||||
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- sheield dashboard -->
|
||||
<!--
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Deal Stage Wise Progress</h3>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="isLoading">
|
||||
<canvas
|
||||
baseChart
|
||||
[data]="barChartData"
|
||||
[options]="barChartOptions"
|
||||
[type]="barChartType"
|
||||
(chartClick)="chartClicked($event)"
|
||||
(chartHover)="chartHovered($event)">
|
||||
</canvas>
|
||||
<div class="loading-overlay" *ngIf="isLoading">
|
||||
<div class="shimmer-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,278 @@
|
||||
// Chart container structure
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Filter section styling
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// Chart header styling
|
||||
.chart-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chart wrapper and content
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 300px; // Ensure minimum height for chart
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
|
||||
.chart-display {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chart-display {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
|
||||
.shimmer-bar {
|
||||
width: 80%;
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.chart-container {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
min-height: 250px; // Adjust for mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,310 @@
|
||||
<div style="display:block">
|
||||
<canvas baseChart
|
||||
[datasets]="bubbleChartData"
|
||||
[type]="bubbleChartType"
|
||||
[options]="bubbleChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
</div>
|
||||
<div style="display:block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Bubble Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart container -->
|
||||
<div style="position: relative; height: calc(100% - 80px); width: 100%; padding: 0 10px 30px 10px;">
|
||||
<!-- Loading indicator -->
|
||||
<div *ngIf="!dataLoaded" style="text-align: center; padding: 20px; color: #666; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
|
||||
Loading data...
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="dataLoaded && (noDataAvailable || !isChartDataValid())" style="text-align: center; padding: 20px; color: #666; font-style: italic; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%;">
|
||||
No data available
|
||||
</div>
|
||||
|
||||
<!-- Chart display - Always render the canvas but conditionally show/hide with CSS -->
|
||||
<canvas baseChart
|
||||
[datasets]="bubbleChartData"
|
||||
[type]="bubbleChartType"
|
||||
[options]="bubbleChartOptions"
|
||||
(chartHover)="chartHovered($event)"
|
||||
(chartClick)="chartClicked($event)"
|
||||
[style.visibility]="dataLoaded && !noDataAvailable && isChartDataValid() ? 'visible' : 'hidden'"
|
||||
[style.position]="'absolute'"
|
||||
[style.top]="'0'"
|
||||
[style.left]="'0'"
|
||||
[style.height]="'100%'"
|
||||
[style.width]="'100%'"
|
||||
[style.padding]="'0 10px 20px 10px'">
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user