filter
This commit is contained in:
@@ -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,23 @@
|
||||
.chart-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-header {
|
||||
padding: 10px 15px;
|
||||
background: #f8f8f8;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@@ -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,90 @@
|
||||
import { Component, Input, OnInit, OnDestroy, ComponentRef, ViewChild, ViewContainerRef } 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();
|
||||
}
|
||||
}
|
||||
|
||||
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,103 @@
|
||||
.common-filter-container {
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.presets-section {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.preset-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
select {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.save-preset-section {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.filters-form {
|
||||
.filters-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
|
||||
.filter-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.filter-label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-type-select {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.clr-form-control {
|
||||
margin-bottom: 10px;
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.date-range-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.clr-form-control {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-filters {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
@@ -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,173 @@
|
||||
import { Component, OnInit, OnDestroy } 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 {
|
||||
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());
|
||||
}
|
||||
|
||||
// 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 {
|
||||
this.filterService.updateFilterValue(filterId, value);
|
||||
}
|
||||
|
||||
// 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,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,191 @@
|
||||
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 {
|
||||
const currentState = this.filterStateSubject.value;
|
||||
this.filterStateSubject.next({
|
||||
...currentState,
|
||||
[filterId]: value
|
||||
});
|
||||
}
|
||||
|
||||
// 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,3 @@
|
||||
export * from './filter.service';
|
||||
export * from './common-filter.component';
|
||||
export * from './chart-wrapper.component';
|
||||
@@ -22,6 +22,8 @@ import { AlertsService } from 'src/app/services/fnd/alerts.service';
|
||||
import { isArray } from 'highcharts';
|
||||
// Add the SureconnectService import
|
||||
import { SureconnectService } from '../sureconnect/sureconnect.service';
|
||||
// Add the CommonFilterComponent import
|
||||
import { CommonFilterComponent } from '../common-filter/common-filter.component';
|
||||
|
||||
function isNullArray(arr) {
|
||||
return !Array.isArray(arr) || arr.length === 0;
|
||||
@@ -45,6 +47,10 @@ export class EditnewdashComponent implements OnInit {
|
||||
public commonFilterForm: FormGroup; // Add common filter form
|
||||
|
||||
WidgetsMock: WidgetModel[] = [
|
||||
{
|
||||
name: 'Common Filter',
|
||||
identifier: 'common_filter'
|
||||
},
|
||||
{
|
||||
name: 'Radar Chart',
|
||||
identifier: 'radar_chart'
|
||||
@@ -104,6 +110,7 @@ export class EditnewdashComponent implements OnInit {
|
||||
public dashArr: [];
|
||||
|
||||
protected componentCollection = [
|
||||
{ name: "Common Filter", componentInstance: CommonFilterComponent },
|
||||
{ name: "Line Chart", componentInstance: LineChartComponent },
|
||||
{ name: "Doughnut Chart", componentInstance: DoughnutChartComponent },
|
||||
{ name: "Radar Chart", componentInstance: RadarChartComponent },
|
||||
@@ -500,6 +507,16 @@ export class EditnewdashComponent implements OnInit {
|
||||
component: ToDoChartComponent,
|
||||
name: "To Do Chart"
|
||||
});
|
||||
case "common_filter":
|
||||
return this.dashboardArray.push({
|
||||
cols: 10,
|
||||
rows: 3,
|
||||
x: 0,
|
||||
y: 0,
|
||||
chartid: maxChartId + 1,
|
||||
component: CommonFilterComponent,
|
||||
name: "Common Filter"
|
||||
});
|
||||
case "grid_view":
|
||||
return this.dashboardArray.push({
|
||||
cols: 5,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-chart',
|
||||
@@ -57,9 +58,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -135,7 +147,49 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
|
||||
@@ -313,6 +367,36 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (drilldownFilterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(drilldownFilterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
|
||||
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
|
||||
@@ -426,6 +510,11 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Ensure labels and data arrays have the same length
|
||||
private syncLabelAndDataArrays(): void {
|
||||
// For bar charts, we need to ensure all datasets have the same number of data points
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-doughnut-chart',
|
||||
@@ -97,9 +99,23 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Validate initial data
|
||||
this.validateChartData();
|
||||
this.fetchChartData();
|
||||
@@ -180,6 +196,15 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -212,7 +237,49 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-line-chart',
|
||||
@@ -83,9 +85,23 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -122,6 +138,15 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -154,7 +179,49 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -313,6 +380,35 @@ export class LineChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pie-chart',
|
||||
@@ -96,7 +98,13 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Force chart redraw
|
||||
@@ -108,6 +116,14 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
// Validate initial data
|
||||
this.validateChartData();
|
||||
@@ -140,6 +156,15 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
refreshData(): void {
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -172,7 +197,49 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -362,6 +429,35 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
|
||||
Reference in New Issue
Block a user