@axa-fr/slimfaas-planet-saver 0.30.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/package.json +54 -0
- package/src/SlimFaasPlanetSaver.js +200 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @axa-fr/slimfaas-planet-saver
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/%40axa-fr%2Fslimfaas-planet-saver)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
A Vanilla JS project to save the planet. SlimFaas (https://github.com/AxaFrance/slimfaas) is the slimest and simplest Function As A Service on Kubernetes.
|
|
8
|
+
It works as a proxy that you can be deployed in your namespace.
|
|
9
|
+
|
|
10
|
+
SlimFaas API can give to the frontend information about the infrastructure state. **It is a mind changer !**
|
|
11
|
+
|
|
12
|
+
**Why?**
|
|
13
|
+
|
|
14
|
+
Because in production instead of setting up 2 replicas of your API backend, you can set up 0 replicas and use an UX that will show the user that the backend is down instead !
|
|
15
|
+
**@axa-fr/slimfaas-planet-saver** is here to for doing that easily.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
## Getting Started
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
npm install @axa-fr/slimfaas-planet-saver
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Example usage with react :
|
|
26
|
+
```javascript
|
|
27
|
+
import React, { useState, useEffect } from 'react';
|
|
28
|
+
import SlimFaasPlanetSaver from "@axa-fr/slimfaas-planet-saver";
|
|
29
|
+
|
|
30
|
+
const PlanetSaver = ({ children, baseUrl, fetch }) => {
|
|
31
|
+
const [isFirstStart, setIsFirstStart] = useState(true); // State for first start
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!baseUrl) return;
|
|
35
|
+
|
|
36
|
+
const environmentStarter = new SlimFaasPlanetSaver(baseUrl, {
|
|
37
|
+
interval: 2000,
|
|
38
|
+
fetch,
|
|
39
|
+
updateCallback: (data) => {
|
|
40
|
+
const allReady = data.every((item) => item.NumberReady >= 1);
|
|
41
|
+
if (allReady && isFirstStart) {
|
|
42
|
+
setIsFirstStart(false);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
errorCallback: (error) => {
|
|
46
|
+
console.error('Error detected :', error);
|
|
47
|
+
},
|
|
48
|
+
overlayStartingMessage: 'Starting the environment...',
|
|
49
|
+
overlayNoActivityMessage: 'Waiting activity to start environment...',
|
|
50
|
+
overlayErrorMessage: 'An error occured when starting environment. Please contact an administrator.',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Start polling
|
|
54
|
+
environmentStarter.startPolling();
|
|
55
|
+
|
|
56
|
+
// Cleanup
|
|
57
|
+
return () => environmentStarter.cleanup();
|
|
58
|
+
}, [baseUrl, isFirstStart]);
|
|
59
|
+
|
|
60
|
+
// During the first start, display a loading message
|
|
61
|
+
if (isFirstStart) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Once the environment is started, display the children
|
|
66
|
+
return <>{children}</>;
|
|
67
|
+
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default PlanetSaver;
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Run the demo
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
git clone https://github.com/AxaFrance/slimfaas.git
|
|
78
|
+
cd slimfaas/src/SlimFaasPlanetSaver
|
|
79
|
+
npm i
|
|
80
|
+
npm run dev
|
|
81
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@axa-fr/slimfaas-planet-saver",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.30.4",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "./src/SlimFaasPlanetSaver.js",
|
|
7
|
+
"description": "Pure vanilla javascript which call SlimFaas API to convert infrastructures resilience to an UX resilience and help to save the planet",
|
|
8
|
+
"files": [
|
|
9
|
+
"src/SlimFaasPlanetSaver.js",
|
|
10
|
+
"README.md",
|
|
11
|
+
"package.json",
|
|
12
|
+
"package-lock.json"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"Faas",
|
|
16
|
+
"SlimFaas",
|
|
17
|
+
"Function As A Service",
|
|
18
|
+
"vanilla",
|
|
19
|
+
"GreenIT"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/AxaFrance/slimfaas.git"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "vite",
|
|
27
|
+
"build": "vite build",
|
|
28
|
+
"lint": "eslint .",
|
|
29
|
+
"preview": "vite preview"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"react": "^18.3.1",
|
|
34
|
+
"react-dom": "^18.3.1",
|
|
35
|
+
"@chromatic-com/storybook": "3.2.2",
|
|
36
|
+
"@eslint/js": "^9.15.0",
|
|
37
|
+
"@types/react": "^18.3.12",
|
|
38
|
+
"@types/react-dom": "^18.3.1",
|
|
39
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
40
|
+
"eslint": "^9.15.0",
|
|
41
|
+
"eslint-plugin-react": "^7.37.2",
|
|
42
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
43
|
+
"eslint-plugin-react-refresh": "^0.4.14",
|
|
44
|
+
"eslint-plugin-storybook": "0.11.1",
|
|
45
|
+
"globals": "^15.12.0",
|
|
46
|
+
"prop-types": "15.8.1",
|
|
47
|
+
"vite": "^6.0.1"
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"registry": "https://registry.npmjs.org/"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const normalizeBaseUrl = (url) => {
|
|
2
|
+
let tempUrl = url;
|
|
3
|
+
if (tempUrl.endsWith('/')) tempUrl = tempUrl.slice(0, -1);
|
|
4
|
+
return tempUrl;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default class SlimFaasPlanetSaver {
|
|
8
|
+
constructor(baseUrl, options = {}) {
|
|
9
|
+
this.baseUrl = normalizeBaseUrl(baseUrl);
|
|
10
|
+
this.updateCallback = options.updateCallback || (() => {});
|
|
11
|
+
this.errorCallback = options.errorCallback || (() => {});
|
|
12
|
+
this.interval = options.interval || 5000;
|
|
13
|
+
this.overlayStartingMessage = options.overlayStartingMessage || 'Starting in progress...';
|
|
14
|
+
this.overlayNoActivityMessage = options.overlayNoActivityMessage || 'Waiting activity to start environment...';
|
|
15
|
+
this.overlayErrorMessage = options.overlayErrorMessage || 'An error occured when starting environment. Please contact an administrator.';
|
|
16
|
+
this.fetch = options.fetch || fetch;
|
|
17
|
+
this.intervalId = null;
|
|
18
|
+
this.isDocumentVisible = !document.hidden;
|
|
19
|
+
this.overlayElement = null;
|
|
20
|
+
this.styleElement = null;
|
|
21
|
+
this.isReady = false;
|
|
22
|
+
|
|
23
|
+
// Initialize lastMouseMoveTime and bind the handler
|
|
24
|
+
this.lastMouseMoveTime = Date.now();
|
|
25
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
26
|
+
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
|
|
27
|
+
|
|
28
|
+
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
29
|
+
document.addEventListener('mousemove', this.handleMouseMove);
|
|
30
|
+
|
|
31
|
+
this.createOverlay();
|
|
32
|
+
this.injectStyles();
|
|
33
|
+
|
|
34
|
+
this.events = document.createElement('div');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
handleMouseMove() {
|
|
38
|
+
this.lastMouseMoveTime = Date.now();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
handleVisibilityChange() {
|
|
42
|
+
this.isDocumentVisible = !document.hidden;
|
|
43
|
+
if (this.isDocumentVisible) {
|
|
44
|
+
this.startPolling();
|
|
45
|
+
} else {
|
|
46
|
+
this.stopPolling();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async wakeUpPods(data) {
|
|
51
|
+
const wakePromises = data
|
|
52
|
+
.filter((item) => item.NumberReady === 0)
|
|
53
|
+
.map((item) =>
|
|
54
|
+
this.fetch(`${this.baseUrl}/wake-function/${item.Name}`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await Promise.all(wakePromises);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("Error waking up pods:", error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async fetchStatus() {
|
|
67
|
+
try {
|
|
68
|
+
const response = await this.fetch(`${this.baseUrl}/status-functions`);
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
71
|
+
}
|
|
72
|
+
const data = await response.json();
|
|
73
|
+
|
|
74
|
+
const allReady = data.every((item) => item.NumberReady >= 1);
|
|
75
|
+
this.setReadyState(allReady);
|
|
76
|
+
|
|
77
|
+
this.updateCallback(data);
|
|
78
|
+
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const mouseMovedRecently = now - this.lastMouseMoveTime <= 60000; // 1 minute in milliseconds
|
|
81
|
+
|
|
82
|
+
if (!allReady && this.isDocumentVisible && !mouseMovedRecently) {
|
|
83
|
+
this.updateOverlayMessage(this.overlayNoActivityMessage);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!this.isDocumentVisible || mouseMovedRecently) {
|
|
87
|
+
this.updateOverlayMessage(this.overlayStartingMessage);
|
|
88
|
+
await this.wakeUpPods(data);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const errorMessage = error.message;
|
|
92
|
+
this.updateOverlayMessage(this.overlayErrorMessage);
|
|
93
|
+
this.errorCallback(errorMessage);
|
|
94
|
+
this.triggerEvent('error', { message: errorMessage });
|
|
95
|
+
console.error('Error getting slimfaas data:', errorMessage);
|
|
96
|
+
} finally {
|
|
97
|
+
this.intervalId = setTimeout(() => {
|
|
98
|
+
this.fetchStatus();
|
|
99
|
+
}, this.interval);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setReadyState(isReady) {
|
|
104
|
+
this.isReady = isReady;
|
|
105
|
+
if (isReady) {
|
|
106
|
+
this.hideOverlay();
|
|
107
|
+
} else {
|
|
108
|
+
this.showOverlay();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
startPolling() {
|
|
113
|
+
if (this.intervalId || !this.baseUrl) return;
|
|
114
|
+
|
|
115
|
+
this.fetchStatus();
|
|
116
|
+
|
|
117
|
+
this.intervalId = setTimeout(() => {
|
|
118
|
+
this.fetchStatus();
|
|
119
|
+
}, this.interval);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
stopPolling() {
|
|
123
|
+
if (this.intervalId) {
|
|
124
|
+
clearTimeout(this.intervalId);
|
|
125
|
+
this.intervalId = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
injectStyles() {
|
|
130
|
+
const cssString = `
|
|
131
|
+
.environment-overlay {
|
|
132
|
+
position: fixed;
|
|
133
|
+
top: 0;
|
|
134
|
+
left: 0;
|
|
135
|
+
width: 100%;
|
|
136
|
+
cursor: not-allowed;
|
|
137
|
+
height: 100%;
|
|
138
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
139
|
+
display: flex;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
align-items: center;
|
|
142
|
+
color: white;
|
|
143
|
+
font-size: 1.5rem;
|
|
144
|
+
font-weight: bold;
|
|
145
|
+
z-index: 1000;
|
|
146
|
+
visibility: hidden;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.environment-overlay.visible {
|
|
150
|
+
visibility: visible;
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
this.styleElement = document.createElement('style');
|
|
155
|
+
this.styleElement.textContent = cssString;
|
|
156
|
+
document.head.appendChild(this.styleElement);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
createOverlay() {
|
|
160
|
+
this.overlayElement = document.createElement('div');
|
|
161
|
+
this.overlayElement.className = 'environment-overlay';
|
|
162
|
+
this.overlayElement.innerText = this.overlayMessage;
|
|
163
|
+
document.body.appendChild(this.overlayElement);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
showOverlay() {
|
|
167
|
+
if (this.overlayElement) {
|
|
168
|
+
this.overlayElement.classList.add('visible');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
hideOverlay() {
|
|
173
|
+
if (this.overlayElement) {
|
|
174
|
+
this.overlayElement.classList.remove('visible');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
updateOverlayMessage(newMessage) {
|
|
179
|
+
if (this.overlayElement) {
|
|
180
|
+
this.overlayElement.innerText = newMessage;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
triggerEvent(eventName, detail) {
|
|
185
|
+
const event = new CustomEvent(eventName, { detail });
|
|
186
|
+
this.events.dispatchEvent(event);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
cleanup() {
|
|
190
|
+
this.stopPolling();
|
|
191
|
+
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
192
|
+
document.removeEventListener('mousemove', this.handleMouseMove); // Remove mousemove listener
|
|
193
|
+
if (this.overlayElement) {
|
|
194
|
+
document.body.removeChild(this.overlayElement);
|
|
195
|
+
}
|
|
196
|
+
if (this.styleElement) {
|
|
197
|
+
document.head.removeChild(this.styleElement);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|