Skip to content

Commit edbf590

Browse files
committed
feat: added healthchecks into configuration pages
1 parent 8fb8b90 commit edbf590

File tree

14 files changed

+326
-26
lines changed

14 files changed

+326
-26
lines changed

kubernetes-deploy/02-phpmyfaq-svc.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,3 @@ spec:
2323
sessionAffinity: None
2424
type: ClusterIP
2525
---
26-

kubernetes-deploy/03-phpmyfaq-ingress.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,3 @@ spec:
2626
path: /
2727
pathType: Prefix
2828
---
29-

phpmyfaq/admin/assets/src/api/elasticsearch.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi, afterEach } from 'vitest';
2-
import { fetchElasticsearchAction, fetchElasticsearchStatistics } from './elasticsearch';
2+
import { fetchElasticsearchAction, fetchElasticsearchStatistics, fetchElasticsearchHealthcheck } from './elasticsearch';
33

44
describe('Elasticsearch API', () => {
55
afterEach(() => {
@@ -88,4 +88,62 @@ describe('Elasticsearch API', () => {
8888
await expect(fetchElasticsearchStatistics()).rejects.toThrow(mockError);
8989
});
9090
});
91+
92+
describe('fetchElasticsearchHealthcheck', () => {
93+
it('should fetch Elasticsearch healthcheck and return JSON response when available', async () => {
94+
const mockResponse = { available: true, status: 'healthy' };
95+
global.fetch = vi.fn(() =>
96+
Promise.resolve({
97+
ok: true,
98+
json: () => Promise.resolve(mockResponse),
99+
} as Response)
100+
);
101+
102+
const result = await fetchElasticsearchHealthcheck();
103+
104+
expect(result).toEqual(mockResponse);
105+
expect(global.fetch).toHaveBeenCalledWith('./api/elasticsearch/healthcheck', {
106+
method: 'GET',
107+
cache: 'no-cache',
108+
headers: {
109+
'Content-Type': 'application/json',
110+
},
111+
redirect: 'follow',
112+
referrerPolicy: 'no-referrer',
113+
});
114+
});
115+
116+
it('should throw an error when Elasticsearch returns 503 Service Unavailable', async () => {
117+
const errorResponse = { available: false, status: 'unavailable' };
118+
global.fetch = vi.fn(() =>
119+
Promise.resolve({
120+
ok: false,
121+
status: 503,
122+
json: () => Promise.resolve(errorResponse),
123+
} as Response)
124+
);
125+
126+
await expect(fetchElasticsearchHealthcheck()).rejects.toThrow('Elasticsearch is unavailable');
127+
});
128+
129+
it('should throw an error with custom message when error data is provided', async () => {
130+
const errorResponse = { error: 'Connection refused' };
131+
global.fetch = vi.fn(() =>
132+
Promise.resolve({
133+
ok: false,
134+
status: 503,
135+
json: () => Promise.resolve(errorResponse),
136+
} as Response)
137+
);
138+
139+
await expect(fetchElasticsearchHealthcheck()).rejects.toThrow('Connection refused');
140+
});
141+
142+
it('should throw an error if fetch fails', async () => {
143+
const mockError = new Error('Network error');
144+
global.fetch = vi.fn(() => Promise.reject(mockError));
145+
146+
await expect(fetchElasticsearchHealthcheck()).rejects.toThrow(mockError);
147+
});
148+
});
91149
});

phpmyfaq/admin/assets/src/api/elasticsearch.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,22 @@ export const fetchElasticsearchStatistics = async (): Promise<ElasticsearchRespo
4242

4343
return await response.json();
4444
};
45+
46+
export const fetchElasticsearchHealthcheck = async (): Promise<Response> => {
47+
const response = await fetch('./api/elasticsearch/healthcheck', {
48+
method: 'GET',
49+
cache: 'no-cache',
50+
headers: {
51+
'Content-Type': 'application/json',
52+
},
53+
redirect: 'follow',
54+
referrerPolicy: 'no-referrer',
55+
});
56+
57+
if (!response.ok) {
58+
const errorData = await response.json();
59+
throw new Error(errorData.error || 'Elasticsearch is unavailable');
60+
}
61+
62+
return await response.json();
63+
};

phpmyfaq/admin/assets/src/api/opensearch.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi, afterEach } from 'vitest';
2-
import { fetchOpenSearchAction, fetchOpenSearchStatistics } from './opensearch';
2+
import { fetchOpenSearchAction, fetchOpenSearchStatistics, fetchOpenSearchHealthcheck } from './opensearch';
33

44
describe('OpenSearch API', () => {
55
afterEach(() => {
@@ -76,4 +76,62 @@ describe('OpenSearch API', () => {
7676
await expect(fetchOpenSearchStatistics()).rejects.toThrow(mockError);
7777
});
7878
});
79+
80+
describe('fetchOpenSearchHealthcheck', () => {
81+
it('should fetch OpenSearch healthcheck and return JSON response when available', async () => {
82+
const mockResponse = { available: true, status: 'healthy' };
83+
global.fetch = vi.fn(() =>
84+
Promise.resolve({
85+
ok: true,
86+
json: () => Promise.resolve(mockResponse),
87+
} as Response)
88+
);
89+
90+
const result = await fetchOpenSearchHealthcheck();
91+
92+
expect(result).toEqual(mockResponse);
93+
expect(global.fetch).toHaveBeenCalledWith('./api/opensearch/healthcheck', {
94+
method: 'GET',
95+
cache: 'no-cache',
96+
headers: {
97+
'Content-Type': 'application/json',
98+
},
99+
redirect: 'follow',
100+
referrerPolicy: 'no-referrer',
101+
});
102+
});
103+
104+
it('should throw an error when OpenSearch returns 503 Service Unavailable', async () => {
105+
const errorResponse = { available: false, status: 'unavailable' };
106+
global.fetch = vi.fn(() =>
107+
Promise.resolve({
108+
ok: false,
109+
status: 503,
110+
json: () => Promise.resolve(errorResponse),
111+
} as Response)
112+
);
113+
114+
await expect(fetchOpenSearchHealthcheck()).rejects.toThrow('OpenSearch is unavailable');
115+
});
116+
117+
it('should throw an error with custom message when error data is provided', async () => {
118+
const errorResponse = { error: 'Connection refused' };
119+
global.fetch = vi.fn(() =>
120+
Promise.resolve({
121+
ok: false,
122+
status: 503,
123+
json: () => Promise.resolve(errorResponse),
124+
} as Response)
125+
);
126+
127+
await expect(fetchOpenSearchHealthcheck()).rejects.toThrow('Connection refused');
128+
});
129+
130+
it('should throw an error if fetch fails', async () => {
131+
const mockError = new Error('Network error');
132+
global.fetch = vi.fn(() => Promise.reject(mockError));
133+
134+
await expect(fetchOpenSearchHealthcheck()).rejects.toThrow(mockError);
135+
});
136+
});
79137
});

phpmyfaq/admin/assets/src/api/opensearch.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,22 @@ export const fetchOpenSearchStatistics = async (): Promise<ElasticsearchResponse
4242

4343
return await response.json();
4444
};
45+
46+
export const fetchOpenSearchHealthcheck = async (): Promise<Response> => {
47+
const response = await fetch('./api/opensearch/healthcheck', {
48+
method: 'GET',
49+
cache: 'no-cache',
50+
headers: {
51+
'Content-Type': 'application/json',
52+
},
53+
redirect: 'follow',
54+
referrerPolicy: 'no-referrer',
55+
});
56+
57+
if (!response.ok) {
58+
const errorData = await response.json();
59+
throw new Error(errorData.error || 'OpenSearch is unavailable');
60+
}
61+
62+
return await response.json();
63+
};

phpmyfaq/admin/assets/src/configuration/elasticsearch.test.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
22
import { handleElasticsearch } from './elasticsearch';
3-
import { fetchElasticsearchAction, fetchElasticsearchStatistics } from '../api/elasticsearch';
3+
import {
4+
fetchElasticsearchAction,
5+
fetchElasticsearchStatistics,
6+
fetchElasticsearchHealthcheck,
7+
} from '../api/elasticsearch';
48

59
vi.mock('../api/elasticsearch');
610
vi.mock('../../../../assets/src/utils');
@@ -16,8 +20,10 @@ describe('Elasticsearch Functions', () => {
1620
document.body.innerHTML = `
1721
<button class="pmf-elasticsearch" data-action="reindex">Reindex</button>
1822
<div id="pmf-elasticsearch-stats"></div>
23+
<div id="pmf-elasticsearch-healthcheck-alert"><span class="alert-message"></span></div>
1924
`;
2025

26+
(fetchElasticsearchHealthcheck as Mock).mockResolvedValue({ available: true, status: 'healthy' });
2127
(fetchElasticsearchAction as Mock).mockResolvedValue({ success: 'Reindexing started' });
2228
(fetchElasticsearchStatistics as Mock).mockResolvedValue({
2329
index: 'test-index',
@@ -41,12 +47,14 @@ describe('Elasticsearch Functions', () => {
4147
expect(fetchElasticsearchAction).toHaveBeenCalledWith('reindex');
4248
});
4349

44-
it('should handle Elasticsearch statistics update', async () => {
50+
it('should handle Elasticsearch statistics update when healthy', async () => {
4551
document.body.innerHTML = `
4652
<button class="pmf-elasticsearch" data-action="reindex">Reindex</button>
4753
<div id="pmf-elasticsearch-stats"></div>
54+
<div id="pmf-elasticsearch-healthcheck-alert"><span class="alert-message"></span></div>
4855
`;
4956

57+
(fetchElasticsearchHealthcheck as Mock).mockResolvedValue({ available: true, status: 'healthy' });
5058
(fetchElasticsearchStatistics as Mock).mockResolvedValue({
5159
index: 'test-index',
5260
stats: {
@@ -67,5 +75,63 @@ describe('Elasticsearch Functions', () => {
6775
expect(statsDiv.innerHTML).toContain('Documents');
6876
expect(statsDiv.innerHTML).toContain('Storage size');
6977
});
78+
79+
it('should display health check alert when Elasticsearch is unavailable', async () => {
80+
document.body.innerHTML = `
81+
<button class="pmf-elasticsearch" data-action="reindex">Reindex</button>
82+
<div id="pmf-elasticsearch-stats"></div>
83+
<div id="pmf-elasticsearch-healthcheck-alert" style="display: none;"><span class="alert-message"></span></div>
84+
`;
85+
86+
(fetchElasticsearchHealthcheck as Mock).mockRejectedValue(new Error('Elasticsearch is unavailable'));
87+
88+
await handleElasticsearch();
89+
90+
const alertDiv = document.getElementById('pmf-elasticsearch-healthcheck-alert') as HTMLElement;
91+
expect(alertDiv.style.display).toBe('block');
92+
expect(alertDiv.querySelector('.alert-message')?.textContent).toBe('Elasticsearch is unavailable');
93+
});
94+
95+
it('should hide health check alert when Elasticsearch is available', async () => {
96+
document.body.innerHTML = `
97+
<button class="pmf-elasticsearch" data-action="reindex">Reindex</button>
98+
<div id="pmf-elasticsearch-stats"></div>
99+
<div id="pmf-elasticsearch-healthcheck-alert" style="display: block;"><span class="alert-message"></span></div>
100+
`;
101+
102+
(fetchElasticsearchHealthcheck as Mock).mockResolvedValue({ available: true, status: 'healthy' });
103+
(fetchElasticsearchStatistics as Mock).mockResolvedValue({
104+
index: 'test-index',
105+
stats: {
106+
indices: {
107+
'test-index': {
108+
total: {
109+
docs: { count: 1000 },
110+
store: { size_in_bytes: 1024 },
111+
},
112+
},
113+
},
114+
},
115+
});
116+
117+
await handleElasticsearch();
118+
119+
const alertDiv = document.getElementById('pmf-elasticsearch-healthcheck-alert') as HTMLElement;
120+
expect(alertDiv.style.display).toBe('none');
121+
});
122+
123+
it('should not fetch statistics when Elasticsearch is unhealthy', async () => {
124+
document.body.innerHTML = `
125+
<button class="pmf-elasticsearch" data-action="reindex">Reindex</button>
126+
<div id="pmf-elasticsearch-stats"></div>
127+
<div id="pmf-elasticsearch-healthcheck-alert"><span class="alert-message"></span></div>
128+
`;
129+
130+
(fetchElasticsearchHealthcheck as Mock).mockRejectedValue(new Error('Service unavailable'));
131+
132+
await handleElasticsearch();
133+
134+
expect(fetchElasticsearchStatistics).not.toHaveBeenCalled();
135+
});
70136
});
71137
});

phpmyfaq/admin/assets/src/configuration/elasticsearch.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,36 @@
1414
*/
1515

1616
import { pushErrorNotification, pushNotification } from '../../../../assets/src/utils';
17-
import { fetchElasticsearchAction, fetchElasticsearchStatistics } from '../api';
17+
import { fetchElasticsearchAction, fetchElasticsearchHealthcheck, fetchElasticsearchStatistics } from '../api';
1818
import { ElasticsearchResponse, Response } from '../interfaces';
1919
import { formatBytes } from '../utils';
2020

2121
export const handleElasticsearch = async (): Promise<void> => {
2222
const buttons: NodeListOf<HTMLButtonElement> = document.querySelectorAll('button.pmf-elasticsearch');
2323

24+
// Check health status on page load
25+
const healthCheckAlert = async (): Promise<boolean> => {
26+
const alertDiv = document.getElementById('pmf-elasticsearch-healthcheck-alert') as HTMLElement;
27+
if (alertDiv) {
28+
try {
29+
await fetchElasticsearchHealthcheck();
30+
alertDiv.style.display = 'none';
31+
return true;
32+
} catch (error) {
33+
alertDiv.style.display = 'block';
34+
const alertMessage = alertDiv.querySelector('.alert-message');
35+
if (alertMessage) {
36+
alertMessage.textContent = error instanceof Error ? error.message : 'Elasticsearch is unavailable';
37+
}
38+
return false;
39+
}
40+
}
41+
return false;
42+
};
43+
44+
// Run health check on page load
45+
const isHealthy = await healthCheckAlert();
46+
2447
if (buttons) {
2548
buttons.forEach((element: HTMLButtonElement): void => {
2649
element.addEventListener('click', async (event: Event): Promise<void> => {
@@ -67,7 +90,10 @@ export const handleElasticsearch = async (): Promise<void> => {
6790
}
6891
};
6992

70-
elasticsearchStats();
93+
// Only fetch stats if Elasticsearch is healthy
94+
if (isHealthy) {
95+
elasticsearchStats();
96+
}
7197
});
7298
}
7399
};

phpmyfaq/admin/assets/src/configuration/opensearch.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,36 @@
1414
*/
1515

1616
import { pushErrorNotification, pushNotification } from '../../../../assets/src/utils';
17-
import { fetchOpenSearchAction, fetchOpenSearchStatistics } from '../api';
17+
import { fetchOpenSearchAction, fetchOpenSearchHealthcheck, fetchOpenSearchStatistics } from '../api';
1818
import { ElasticsearchResponse, Response } from '../interfaces';
1919
import { formatBytes } from '../utils';
2020

2121
export const handleOpenSearch = async (): Promise<void> => {
2222
const buttons: NodeListOf<HTMLButtonElement> = document.querySelectorAll('button.pmf-opensearch');
2323

24+
// Check health status on page load
25+
const healthCheckAlert = async (): Promise<boolean> => {
26+
const alertDiv = document.getElementById('pmf-opensearch-healthcheck-alert') as HTMLElement;
27+
if (alertDiv) {
28+
try {
29+
await fetchOpenSearchHealthcheck();
30+
alertDiv.style.display = 'none';
31+
return true;
32+
} catch (error) {
33+
alertDiv.style.display = 'block';
34+
const alertMessage = alertDiv.querySelector('.alert-message');
35+
if (alertMessage) {
36+
alertMessage.textContent = error instanceof Error ? error.message : 'OpenSearch is unavailable';
37+
}
38+
return false;
39+
}
40+
}
41+
return false;
42+
};
43+
44+
// Run health check on page load
45+
const isHealthy = await healthCheckAlert();
46+
2447
if (buttons) {
2548
buttons.forEach((element: HTMLButtonElement): void => {
2649
element.addEventListener('click', async (event: Event): Promise<void> => {
@@ -67,7 +90,10 @@ export const handleOpenSearch = async (): Promise<void> => {
6790
}
6891
};
6992

70-
openSearchStats();
93+
// Only fetch stats if OpenSearch is healthy
94+
if (isHealthy) {
95+
openSearchStats();
96+
}
7197
});
7298
}
7399
};

0 commit comments

Comments
 (0)