Skip to content

Commit 33e4f8d

Browse files
authored
Merge pull request buerokratt#1184 from 1AhmedYasser/Allow-only-publishable-services-to-become-Active
Allow only publishable services to become Active
2 parents 2a05017 + df13a91 commit 33e4f8d

10 files changed

Lines changed: 402 additions & 42 deletions

File tree

‎GUI/src/components/ServicesTable/ServicesTable.scss‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,57 @@
1919
color: var(--dark-text-label);
2020
}
2121
}
22+
23+
.service-state-cell {
24+
display: grid;
25+
place-items: center;
26+
27+
> * {
28+
grid-area: 1 / 1;
29+
}
30+
}
31+
32+
.service-state-spinner {
33+
width: 12px;
34+
height: 12px;
35+
border: 2px solid currentColor;
36+
border-right-color: transparent;
37+
border-radius: 50%;
38+
animation: spin 0.6s linear infinite;
39+
}
40+
41+
.activation-blockers-list {
42+
display: flex;
43+
flex-direction: column;
44+
gap: 4px;
45+
max-height: 400px;
46+
margin: 0;
47+
padding: 0;
48+
overflow-y: auto;
49+
list-style: none;
50+
51+
&__item {
52+
width: 100%;
53+
padding: 8px 12px;
54+
border: none;
55+
border-radius: 4px;
56+
background: transparent;
57+
text-align: left;
58+
cursor: pointer;
59+
font: inherit;
60+
color: #005aa3;
61+
62+
&:hover {
63+
background: rgba(0, 90, 163, 0.08);
64+
}
65+
66+
&--static {
67+
cursor: default;
68+
color: inherit;
69+
70+
&:hover {
71+
background: transparent;
72+
}
73+
}
74+
}
75+
}

‎GUI/src/components/ServicesTable/columns.tsx‎

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,23 @@ import useServiceListStore from 'store/services.store';
1212
import useStore from 'store/store';
1313
import useToastStore from 'store/toasts.store';
1414
import { Service, ServiceState } from 'types';
15+
import { getServiceStateLabelType } from 'utils/service-state-label';
1516

1617
interface GetColumnsConfig {
1718
isCommon: boolean;
1819
navigate: NavigateFunction;
1920
hideDeletePopup: () => void;
2021
showReadyPopup: () => void;
22+
serviceIdBeingChecked: string | null;
2123
}
2224

23-
export const getColumns = ({ isCommon, navigate, hideDeletePopup, showReadyPopup }: GetColumnsConfig) => {
25+
export const getColumns = ({
26+
isCommon,
27+
navigate,
28+
hideDeletePopup,
29+
showReadyPopup,
30+
serviceIdBeingChecked,
31+
}: GetColumnsConfig) => {
2432
const columnHelper = createColumnHelper<Service>();
2533
const userInfo = useStore.getState().userInfo;
2634

@@ -150,22 +158,32 @@ export const getColumns = ({ isCommon, navigate, hideDeletePopup, showReadyPopup
150158
meta: {
151159
size: 120,
152160
},
153-
cell: (props) => (
154-
<Track
155-
justify="start"
156-
onClick={() => {
157-
useServiceListStore.getState().setSelectedService(props.row.original);
158-
const state = props.row.original.state;
159-
if (state === ServiceState.Ready) {
160-
showReadyPopup();
161-
}
162-
}}
163-
>
164-
<Label type={getLabelType(props.row.original.state)}>
165-
{i18n.t(`overview.service.states.${props.row.original.state}`)}
166-
</Label>
167-
</Track>
168-
),
161+
cell: (props) => {
162+
const isServiceMatch = serviceIdBeingChecked === props.row.original.serviceId;
163+
164+
return (
165+
<Track
166+
justify="start"
167+
onClick={() => {
168+
if (isServiceMatch) return;
169+
useServiceListStore.getState().setSelectedService(props.row.original);
170+
const state = props.row.original.state;
171+
if (state === ServiceState.Ready) {
172+
showReadyPopup();
173+
}
174+
}}
175+
>
176+
<Label type={getServiceStateLabelType(props.row.original.state)}>
177+
<span className="service-state-cell">
178+
<span style={{ visibility: isServiceMatch ? 'hidden' : 'visible' }}>
179+
{i18n.t(`overview.service.states.${props.row.original.state}`)}
180+
</span>
181+
{isServiceMatch && <span className="service-state-spinner" />}
182+
</span>
183+
</Label>
184+
</Track>
185+
);
186+
},
169187
}),
170188
columnHelper.display({
171189
id: 'edit',
@@ -237,18 +255,3 @@ export const getColumns = ({ isCommon, navigate, hideDeletePopup, showReadyPopup
237255
}),
238256
];
239257
};
240-
241-
const getLabelType = (serviceState: ServiceState) => {
242-
switch (serviceState) {
243-
case ServiceState.Ready:
244-
return 'warning-dark';
245-
case ServiceState.Active:
246-
return 'success-light';
247-
case ServiceState.Draft:
248-
return 'disabled';
249-
case ServiceState.Inactive:
250-
return 'warning-dark';
251-
default:
252-
return 'info';
253-
}
254-
};

‎GUI/src/components/ServicesTable/index.tsx‎

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { PaginationState, SortingState } from '@tanstack/react-table';
22
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
33
import { useTranslation } from 'react-i18next';
44
import { useNavigate } from 'react-router-dom';
5+
import { ROUTES } from 'resources/routes-constants';
56
import useServiceListStore from 'store/services.store';
7+
import useToastStore from 'store/toasts.store';
8+
import { ActivationBlocker } from 'types/activation-blocker';
9+
import { getServiceStateLabelType } from 'utils/service-state-label';
610

7-
import { Button, Card, Modal, Track } from '..';
11+
import { Button, Card, Label, Modal, Track } from '..';
812
import DataTable from '../DataTable';
913
import { getColumns } from './columns';
1014

@@ -27,6 +31,8 @@ const getStoredPageSize = (): number => {
2731
const ServicesTable: FC<ServicesTableProps> = ({ isCommon = false }) => {
2832
const { t } = useTranslation();
2933
const [isDeletePopupVisible, setIsDeletePopupVisible] = useState(false);
34+
const [activationBlockers, setActivationBlockers] = useState<ActivationBlocker[] | null>(null);
35+
const [serviceIdBeingChecked, setServiceIdBeingChecked] = useState<string | null>(null);
3036
const services = useServiceListStore((state) => (isCommon ? state.commonServices : state.notCommonServices));
3137
const navigate = useNavigate();
3238
const [pagination, setPagination] = useState<PaginationState>({
@@ -55,7 +61,7 @@ const ServicesTable: FC<ServicesTableProps> = ({ isCommon = false }) => {
5561

5662
const changeServiceState = useCallback(
5763
(activate: boolean = false, draft: boolean = false) => {
58-
useServiceListStore
64+
return useServiceListStore
5965
.getState()
6066
.changeServiceState(
6167
() => {},
@@ -66,24 +72,44 @@ const ServicesTable: FC<ServicesTableProps> = ({ isCommon = false }) => {
6672
pagination,
6773
sorting,
6874
)
69-
.then(() => {})
7075
.catch((e) => {
7176
console.error(e);
7277
});
7378
},
7479
[t, pagination, sorting],
7580
);
7681

82+
const attemptActivation = useCallback(async () => {
83+
const service = useServiceListStore.getState().selectedService;
84+
if (!service || serviceIdBeingChecked) return;
85+
86+
setServiceIdBeingChecked(service.serviceId);
87+
try {
88+
const blockers = await useServiceListStore.getState().loadActivationBlockers(service);
89+
if (blockers.length > 0) {
90+
setActivationBlockers(blockers);
91+
return;
92+
}
93+
await changeServiceState(true);
94+
} catch (e) {
95+
console.error(e);
96+
useToastStore.getState().error({ title: t('overview.service.toast.failed.state') });
97+
} finally {
98+
setServiceIdBeingChecked(null);
99+
}
100+
}, [changeServiceState, serviceIdBeingChecked, t]);
101+
77102
const columns = useMemo(() => {
78103
return getColumns({
79104
isCommon,
80105
navigate,
81106
hideDeletePopup: () => setIsDeletePopupVisible(true),
82107
showReadyPopup: () => {
83-
changeServiceState(true);
108+
void attemptActivation();
84109
},
110+
serviceIdBeingChecked,
85111
});
86-
}, [isCommon, changeServiceState, navigate]);
112+
}, [isCommon, attemptActivation, navigate, serviceIdBeingChecked]);
87113

88114
const deleteSelectedService = () => {
89115
setIsDeletingService(true);
@@ -122,6 +148,51 @@ const ServicesTable: FC<ServicesTableProps> = ({ isCommon = false }) => {
122148
</Track>
123149
</Modal>
124150
)}
151+
{activationBlockers && activationBlockers.length > 0 && (
152+
<Modal
153+
title={t('overview.popup.activationBlocked.title')}
154+
onClose={() => setActivationBlockers(null)}
155+
footer={
156+
<Button appearance="primary" onClick={() => setActivationBlockers(null)}>
157+
{t('global.continue')}
158+
</Button>
159+
}
160+
>
161+
<ul className="activation-blockers-list">
162+
{activationBlockers.map((blocker) => {
163+
const content = (
164+
<Track justify="between">
165+
<strong>{blocker.name}</strong>
166+
<Label type={getServiceStateLabelType(blocker.state)}>
167+
{t(`overview.service.states.${blocker.state}`)}
168+
</Label>
169+
</Track>
170+
);
171+
172+
return (
173+
<li key={blocker.serviceId}>
174+
{blocker.state === 'missing' ? (
175+
<div className="activation-blockers-list__item activation-blockers-list__item--static">
176+
{content}
177+
</div>
178+
) : (
179+
<button
180+
type="button"
181+
className="activation-blockers-list__item"
182+
onClick={() => {
183+
setActivationBlockers(null);
184+
navigate(ROUTES.replaceWithId(ROUTES.EDITSERVICE_ROUTE, blocker.serviceId));
185+
}}
186+
>
187+
{content}
188+
</button>
189+
)}
190+
</li>
191+
);
192+
})}
193+
</ul>
194+
</Modal>
195+
)}
125196
<DataTable
126197
sortable
127198
data={services}

‎GUI/src/i18n/en/common.json‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@
178178
"active": "Active",
179179
"inactive": "Inactive",
180180
"draft": "Draft",
181-
"ready": "Ready"
181+
"ready": "Ready",
182+
"missing": "Missing"
182183
},
183184
"toast": {
184185
"deleted": "Service has been successfully deleted",
@@ -217,7 +218,10 @@
217218
"noSlotsAvailable": "No slots available",
218219
"connectionQuestion": "Are you sure you want to connect to this intent?",
219220
"createNewIntent": "Create new intent",
220-
"deleteNodeConnections": "Would you like to also delete all the logic paths connected to this node or leave them hanging?"
221+
"deleteNodeConnections": "Would you like to also delete all the logic paths connected to this node or leave them hanging?",
222+
"activationBlocked": {
223+
"title": "Cannot activate service. The following connected services must be Active:"
224+
}
221225
}
222226
},
223227
"connectionRequests": {

‎GUI/src/i18n/et/common.json‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@
178178
"active": "Aktiivne",
179179
"inactive": "Mitteaktiivne",
180180
"draft": "Mustand",
181-
"ready": "Valmis"
181+
"ready": "Valmis",
182+
"missing": "Puudub"
182183
},
183184
"toast": {
184185
"deleted": "Teenus kustutatud",
@@ -217,7 +218,10 @@
217218
"noSlotsAvailable": "Ühtegi pesa pole saadaval",
218219
"connectionQuestion": "Kas olete kindel, et soovite teenuse selle teemaga ühendada?",
219220
"createNewIntent": "Loo uus teema",
220-
"deleteNodeConnections": "Kas soovid kustutada kõik loogikateed, mis on seotud selle plokiga, või jätta need alles?"
221+
"deleteNodeConnections": "Kas soovid kustutada kõik loogikateed, mis on seotud selle plokiga, või jätta need alles?",
222+
"activationBlocked": {
223+
"title": "Teenust ei saa aktiveerida. Järgmised seotud teenused peavad olema aktiivsed:"
224+
}
221225
}
222226
},
223227
"connectionRequests": {

‎GUI/src/store/services.store.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
import { PaginationState, SortingState } from '@tanstack/react-table';
2-
import { changeServiceStatus, deleteService as deleteServiceApi, getServicesList } from 'resources/api-constants';
2+
import { Node } from '@xyflow/react';
3+
import {
4+
changeServiceStatus,
5+
deleteService as deleteServiceApi,
6+
getServiceById,
7+
getServicesList,
8+
} from 'resources/api-constants';
39
import { Service, ServiceState } from 'types';
10+
import { ActivationBlocker } from 'types/activation-blocker';
11+
import { findActivationBlockers, ServiceFlowLookup } from 'utils/service-activation';
412
import { create } from 'zustand';
513
import { persist } from 'zustand/middleware';
614

715
import useToastStore from './toasts.store';
816
import api from '../services/api-dev';
917

18+
const fetchServiceFlow = async (serviceId: string): Promise<ServiceFlowLookup | undefined> => {
19+
try {
20+
const response = await api.post<Service>(getServiceById(), { id: serviceId, search: '' });
21+
const data = response.data;
22+
if (!data?.serviceId) return undefined;
23+
24+
const structure = JSON.parse(data.structure?.value ?? '{}');
25+
return { name: data.name, state: data.state, nodes: (structure?.nodes ?? []) as Node[] };
26+
} catch (error) {
27+
console.error(error);
28+
return undefined;
29+
}
30+
};
31+
1032
interface ServiceStoreState {
1133
services: Service[];
1234
commonServices: Service[];
@@ -24,6 +46,7 @@ interface ServiceStoreState {
2446
deleteService: (id: string) => void;
2547
selectedService: Service | undefined;
2648
setSelectedService: (service: Service) => void;
49+
loadActivationBlockers: (service: Service) => Promise<ActivationBlocker[]>;
2750
changeServiceState: (
2851
onEnd: () => void,
2952
successMessage: string,
@@ -136,6 +159,11 @@ const useServiceListStore = create<ServiceStoreState>()(
136159
selectedService: service,
137160
});
138161
},
162+
loadActivationBlockers: async (service: Service) => {
163+
const rootFlow = await fetchServiceFlow(service.serviceId);
164+
if (!rootFlow) return [];
165+
return findActivationBlockers(rootFlow.nodes, fetchServiceFlow);
166+
},
139167
changeServiceState: async (onEnd, successMessage, errorMessage, activate, draft, pagination, sorting) => {
140168
const selectedService = get().selectedService;
141169
if (!selectedService) return;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ServiceState } from './service-state';
2+
3+
export interface ActivationBlocker {
4+
readonly serviceId: string;
5+
readonly name: string;
6+
readonly state: ServiceState | 'missing';
7+
}

0 commit comments

Comments
 (0)