Skip to content

Commit d5a8ee3

Browse files
authored
refactor: project-state cleanup (#3094)
A bit of refactor on the project state, to use more classes and be a bit easy to follow some decissions made. Signed-off-by: Eloy Coto <eloy.coto@acalustra.com>
1 parent 6b8046b commit d5a8ee3

6 files changed

Lines changed: 336 additions & 35 deletions

File tree

workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/index.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
MigrationPhase,
3232
Artifact,
3333
Telemetry,
34-
ProjectStatusState,
34+
ProjectState,
3535
DEFAULT_PAGE_ORDER,
3636
DEFAULT_PAGE_SIZE,
3737
IN_MEMORY_SORT_WARN_THRESHOLD,
@@ -116,19 +116,6 @@ export class X2ADatabaseService implements X2ADatabaseServiceApi {
116116
return this.#projectOps.createProject(input, options);
117117
}
118118

119-
/**
120-
* Semantic ordering for ProjectStatusState.
121-
* Lower values appear first in ascending sort.
122-
*/
123-
static readonly STATE_ORDER: Record<ProjectStatusState, number> = {
124-
created: 0,
125-
initializing: 1,
126-
initialized: 2,
127-
inProgress: 3,
128-
failed: 4,
129-
completed: 5,
130-
};
131-
132119
async listProjects(
133120
query: ProjectsGet['query'],
134121
options: {
@@ -195,8 +182,7 @@ export class X2ADatabaseService implements X2ADatabaseServiceApi {
195182
const sign = order === 'asc' ? 1 : -1;
196183

197184
const stateRank = (p: Project): number =>
198-
X2ADatabaseService.STATE_ORDER[p.status?.state as ProjectStatusState] ??
199-
99;
185+
p.status?.state ? ProjectState.from(p.status.state).ordinal : 99;
200186

201187
result.projects.sort((a, b) => {
202188
// Primary: project-level state (created to completed).

workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/projectStatus.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import {
1818
Job,
1919
JobStatus,
2020
Module,
21+
ProjectState,
2122
ProjectStatus,
22-
ProjectStatusState,
2323
} from '@red-hat-developer-hub/backstage-plugin-x2a-common';
2424

2525
export { calculateModuleStatus } from '@red-hat-developer-hub/backstage-plugin-x2a-node';
@@ -36,7 +36,7 @@ export function calculateProjectStatus(
3636
const total = projectModules.length;
3737
if (!initJob && total === 0) {
3838
return {
39-
state: 'created',
39+
state: ProjectState.CREATED.value,
4040
modulesSummary: {
4141
total: 0,
4242
finished: 0,
@@ -76,25 +76,17 @@ export function calculateProjectStatus(
7676
? JobStatus.from(initJob.status)
7777
: undefined;
7878

79-
let state: ProjectStatusState;
80-
if (error > 0) {
81-
state = 'failed'; // At least one module is in error state
82-
} else if (initStatus?.isActive()) {
83-
state = 'initializing'; // Project's init job is running or scheduling
84-
} else if (initStatus?.isSuccess()) {
85-
if (total > 0 && finished === total) {
86-
state = 'completed'; // All modules are in success state
87-
} else if (total === 0 || pending + cancelled === total) {
88-
state = 'initialized'; // Module list is empty or all modules are in pending/cancelled state
89-
} else {
90-
state = 'inProgress'; // At least one module is beyond the pending state
91-
}
92-
} else {
93-
state = 'failed';
94-
}
79+
const state = determineState({
80+
total,
81+
error,
82+
finished,
83+
pending,
84+
cancelled,
85+
initStatus,
86+
});
9587

9688
return {
97-
state,
89+
state: state.value,
9890
modulesSummary: {
9991
total,
10092
finished,
@@ -106,3 +98,23 @@ export function calculateProjectStatus(
10698
},
10799
};
108100
}
101+
102+
function determineState(counts: {
103+
total: number;
104+
error: number;
105+
finished: number;
106+
pending: number;
107+
cancelled: number;
108+
initStatus?: JobStatus;
109+
}): ProjectState {
110+
const { total, error, finished, pending, cancelled, initStatus } = counts;
111+
112+
if (error > 0) return ProjectState.FAILED;
113+
if (initStatus?.isActive()) return ProjectState.INITIALIZING;
114+
if (!initStatus?.isSuccess()) return ProjectState.FAILED;
115+
if (total > 0 && finished === total) return ProjectState.COMPLETED;
116+
if (total === 0 || pending + cancelled === total)
117+
return ProjectState.INITIALIZED;
118+
119+
return ProjectState.IN_PROGRESS;
120+
}

workspaces/x2a/plugins/x2a-common/report.api.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,48 @@ export interface ProjectsProjectIdRunPostRequest {
501501
userPrompt?: string;
502502
}
503503

504+
// @public (undocumented)
505+
export class ProjectState {
506+
// (undocumented)
507+
static all(): readonly ProjectState[];
508+
// (undocumented)
509+
static readonly COMPLETED: ProjectState;
510+
// (undocumented)
511+
static readonly CREATED: ProjectState;
512+
// (undocumented)
513+
equals(other: ProjectState): boolean;
514+
// (undocumented)
515+
static readonly FAILED: ProjectState;
516+
// (undocumented)
517+
static from(raw: string): ProjectState;
518+
// (undocumented)
519+
static readonly IN_PROGRESS: ProjectState;
520+
// (undocumented)
521+
static readonly INITIALIZED: ProjectState;
522+
// (undocumented)
523+
static readonly INITIALIZING: ProjectState;
524+
// (undocumented)
525+
isComplete(): boolean;
526+
// (undocumented)
527+
isCreated(): boolean;
528+
// (undocumented)
529+
isFailed(): boolean;
530+
// (undocumented)
531+
isInitialized(): boolean;
532+
// (undocumented)
533+
isInitializing(): boolean;
534+
// (undocumented)
535+
isInProgress(): boolean;
536+
// (undocumented)
537+
readonly ordinal: number;
538+
// (undocumented)
539+
toString(): string;
540+
// (undocumented)
541+
readonly value: ProjectStatusState;
542+
// (undocumented)
543+
static values(): readonly ProjectStatusState[];
544+
}
545+
504546
// @public (undocumented)
505547
export interface ProjectStatus {
506548
// (undocumented)
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ProjectState } from './ProjectState';
18+
19+
describe('ProjectState', () => {
20+
describe('from', () => {
21+
it('returns ProjectState.CREATED for "created"', () => {
22+
expect(ProjectState.from('created')).toBe(ProjectState.CREATED);
23+
});
24+
25+
it('returns ProjectState.INITIALIZING for "initializing"', () => {
26+
expect(ProjectState.from('initializing')).toBe(ProjectState.INITIALIZING);
27+
});
28+
29+
it('returns ProjectState.INITIALIZED for "initialized"', () => {
30+
expect(ProjectState.from('initialized')).toBe(ProjectState.INITIALIZED);
31+
});
32+
33+
it('returns ProjectState.IN_PROGRESS for "inProgress"', () => {
34+
expect(ProjectState.from('inProgress')).toBe(ProjectState.IN_PROGRESS);
35+
});
36+
37+
it('returns ProjectState.FAILED for "failed"', () => {
38+
expect(ProjectState.from('failed')).toBe(ProjectState.FAILED);
39+
});
40+
41+
it('returns ProjectState.COMPLETED for "completed"', () => {
42+
expect(ProjectState.from('completed')).toBe(ProjectState.COMPLETED);
43+
});
44+
45+
it('throws for an invalid state', () => {
46+
expect(() => ProjectState.from('invalid')).toThrow(
47+
'Invalid project state: "invalid". Valid: created, initializing, initialized, inProgress, failed, completed',
48+
);
49+
});
50+
});
51+
52+
describe('all', () => {
53+
it('returns 6 states in defined order', () => {
54+
const all = ProjectState.all();
55+
expect(all).toHaveLength(6);
56+
expect(all).toEqual([
57+
ProjectState.CREATED,
58+
ProjectState.INITIALIZING,
59+
ProjectState.INITIALIZED,
60+
ProjectState.IN_PROGRESS,
61+
ProjectState.FAILED,
62+
ProjectState.COMPLETED,
63+
]);
64+
});
65+
});
66+
67+
describe('values', () => {
68+
it('returns raw string values for all states', () => {
69+
expect(ProjectState.values()).toEqual([
70+
'created',
71+
'initializing',
72+
'initialized',
73+
'inProgress',
74+
'failed',
75+
'completed',
76+
]);
77+
});
78+
});
79+
80+
describe('individual predicates', () => {
81+
it('isCreated', () => {
82+
expect(ProjectState.CREATED.isCreated()).toBe(true);
83+
expect(ProjectState.INITIALIZING.isCreated()).toBe(false);
84+
});
85+
86+
it('isInitializing', () => {
87+
expect(ProjectState.INITIALIZING.isInitializing()).toBe(true);
88+
expect(ProjectState.CREATED.isInitializing()).toBe(false);
89+
});
90+
91+
it('isInitialized', () => {
92+
expect(ProjectState.INITIALIZED.isInitialized()).toBe(true);
93+
expect(ProjectState.CREATED.isInitialized()).toBe(false);
94+
});
95+
96+
it('isInProgress', () => {
97+
expect(ProjectState.IN_PROGRESS.isInProgress()).toBe(true);
98+
expect(ProjectState.CREATED.isInProgress()).toBe(false);
99+
});
100+
101+
it('isFailed', () => {
102+
expect(ProjectState.FAILED.isFailed()).toBe(true);
103+
expect(ProjectState.CREATED.isFailed()).toBe(false);
104+
});
105+
106+
it('isComplete', () => {
107+
expect(ProjectState.COMPLETED.isComplete()).toBe(true);
108+
expect(ProjectState.CREATED.isComplete()).toBe(false);
109+
});
110+
});
111+
112+
describe('ordinal', () => {
113+
it('assigns sequential ordinals from 0 to 5', () => {
114+
expect(ProjectState.CREATED.ordinal).toBe(0);
115+
expect(ProjectState.INITIALIZING.ordinal).toBe(1);
116+
expect(ProjectState.INITIALIZED.ordinal).toBe(2);
117+
expect(ProjectState.IN_PROGRESS.ordinal).toBe(3);
118+
expect(ProjectState.FAILED.ordinal).toBe(4);
119+
expect(ProjectState.COMPLETED.ordinal).toBe(5);
120+
});
121+
});
122+
123+
describe('toString', () => {
124+
it('returns the raw string value', () => {
125+
expect(ProjectState.CREATED.toString()).toBe('created');
126+
expect(ProjectState.INITIALIZING.toString()).toBe('initializing');
127+
expect(ProjectState.INITIALIZED.toString()).toBe('initialized');
128+
expect(ProjectState.IN_PROGRESS.toString()).toBe('inProgress');
129+
expect(ProjectState.FAILED.toString()).toBe('failed');
130+
expect(ProjectState.COMPLETED.toString()).toBe('completed');
131+
});
132+
});
133+
134+
describe('equals', () => {
135+
it('returns true for same instance', () => {
136+
expect(ProjectState.CREATED.equals(ProjectState.CREATED)).toBe(true);
137+
});
138+
139+
it('returns true for ProjectState.from result (flyweight identity)', () => {
140+
expect(ProjectState.CREATED.equals(ProjectState.from('created'))).toBe(
141+
true,
142+
);
143+
});
144+
145+
it('returns false for different states', () => {
146+
expect(ProjectState.CREATED.equals(ProjectState.FAILED)).toBe(false);
147+
});
148+
});
149+
150+
describe('flyweight identity', () => {
151+
it('from() returns the exact same instance', () => {
152+
expect(ProjectState.from('created')).toBe(ProjectState.CREATED);
153+
expect(ProjectState.from('initializing')).toBe(ProjectState.INITIALIZING);
154+
expect(ProjectState.from('initialized')).toBe(ProjectState.INITIALIZED);
155+
expect(ProjectState.from('inProgress')).toBe(ProjectState.IN_PROGRESS);
156+
expect(ProjectState.from('failed')).toBe(ProjectState.FAILED);
157+
expect(ProjectState.from('completed')).toBe(ProjectState.COMPLETED);
158+
});
159+
});
160+
});

0 commit comments

Comments
 (0)