@spinnaker/core 2025.0.5 → 2025.1.0

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.
Files changed (116) hide show
  1. package/dist/api/ApiService.d.ts +2 -2
  2. package/dist/api/mock/mockHttpUtils.d.ts +2 -2
  3. package/dist/application/listExtractor/AppListExtractor.d.ts +2 -2
  4. package/dist/bootstrap/paramChangedHelper.d.ts +1 -1
  5. package/dist/cloudProvider/providerSelection/ProviderSelectionService.d.ts +1 -1
  6. package/dist/cluster/filter/ClusterFilterService.d.ts +1 -1
  7. package/dist/cluster/task.matcher.d.ts +1 -1
  8. package/dist/domain/IManagedEntity.d.ts +8 -8
  9. package/dist/domain/IServerGroup.d.ts +1 -0
  10. package/dist/domain/IStageTypeConfig.d.ts +1 -1
  11. package/dist/entityTag/notifications/NotificationsPopover.d.ts +1 -1
  12. package/dist/image/image.reader.d.ts +2 -2
  13. package/dist/index.js +1726 -1521
  14. package/dist/index.js.map +1 -1
  15. package/dist/managed/config/Configuration.d.ts +1 -1
  16. package/dist/managed/config/GitIntegration.d.ts +1 -1
  17. package/dist/managed/constraints/registry.d.ts +2 -2
  18. package/dist/managed/graphql/graphql-sdk.d.ts +137 -137
  19. package/dist/managed/managed.states.d.ts +1 -1
  20. package/dist/managed/overview/artifact/ArtifactActionModal.d.ts +1 -1
  21. package/dist/managed/overview/artifact/VersionOperation.d.ts +3 -3
  22. package/dist/managed/overview/artifact/utils.d.ts +2 -2
  23. package/dist/managed/overview/types.d.ts +9 -9
  24. package/dist/managed/resourceHistory/ManagedResourceHistoryModal.d.ts +1 -1
  25. package/dist/managed/resources/ResourceDefinitionModal.d.ts +1 -1
  26. package/dist/managed/resources/resourceRegistry.d.ts +1 -1
  27. package/dist/managed/versionMetadata/MetadataComponents.d.ts +1 -1
  28. package/dist/managed/versionsHistory/types.d.ts +6 -6
  29. package/dist/manifest/ManifestYaml.d.ts +1 -1
  30. package/dist/modal/wizard/WizardPage.d.ts +1 -1
  31. package/dist/navigation/urlParser.d.ts +1 -1
  32. package/dist/pagerDuty/Pager.d.ts +1 -1
  33. package/dist/pipeline/config/actions/pipelineJson/EditPipelineJsonModal.d.ts +1 -1
  34. package/dist/pipeline/config/stages/FormikStageConfig.d.ts +2 -2
  35. package/dist/pipeline/config/stages/bakeManifest/helm/BakeHelmConfigForm.d.ts +1 -0
  36. package/dist/pipeline/config/stages/common/ExecutionDetailsSection.d.ts +1 -1
  37. package/dist/pipeline/config/stages/entityTags/TagEditor.d.ts +1 -1
  38. package/dist/pipeline/config/templates/PipelineTemplateReader.d.ts +1 -1
  39. package/dist/pipeline/config/triggers/artifacts/helm-image/HelmImageArtifactEditor.d.ts +3 -0
  40. package/dist/pipeline/config/validation/anyFieldRequired.validator.d.ts +1 -1
  41. package/dist/pipeline/config/validation/requiredField.validator.d.ts +1 -1
  42. package/dist/plugins/plugin.registry.d.ts +1 -1
  43. package/dist/presentation/Placement.d.ts +1 -1
  44. package/dist/presentation/Popover.d.ts +1 -1
  45. package/dist/presentation/details/Details.d.ts +7 -0
  46. package/dist/presentation/forms/fields/FormField.d.ts +1 -1
  47. package/dist/presentation/forms/fields/FormikExpressionField.d.ts +1 -1
  48. package/dist/presentation/forms/fields/FormikExpressionRegexField.d.ts +1 -1
  49. package/dist/presentation/forms/fields/FormikFormField.d.ts +1 -1
  50. package/dist/presentation/forms/inputs/interface.d.ts +2 -2
  51. package/dist/presentation/forms/validation/categories.d.ts +3 -3
  52. package/dist/presentation/forms/validation/validation.d.ts +3 -3
  53. package/dist/presentation/hooks/useLatestPromise.hook.d.ts +1 -1
  54. package/dist/presentation/modal/showModal.d.ts +1 -1
  55. package/dist/presentation/tables/Table.d.ts +1 -1
  56. package/dist/presentation/tables/TableCell.d.ts +1 -1
  57. package/dist/presentation/tables/TableRow.d.ts +1 -1
  58. package/dist/presentation/tables/standardGridTableLayout.d.ts +1 -1
  59. package/dist/projects/Projects.d.ts +2 -0
  60. package/dist/projects/index.d.ts +1 -0
  61. package/dist/projects/projects.module.d.ts +3 -2
  62. package/dist/reactShims/AngularJSAdapter.d.ts +3 -3
  63. package/dist/search/infrastructure/SearchResultPods.d.ts +1 -1
  64. package/dist/search/infrastructure/infrastructureSearch.service.d.ts +1 -1
  65. package/dist/serverGroup/details/ServerGroupDetailsWrapper.d.ts +1 -1
  66. package/dist/serverGroupManager/index.d.ts +1 -0
  67. package/dist/serverGroupManager/serverGroupManager.states.d.ts +1 -1
  68. package/dist/utils/Logger.d.ts +1 -1
  69. package/dist/utils/feature/Feature.d.ts +58 -0
  70. package/dist/utils/feature/FeatureContext.d.ts +24 -0
  71. package/dist/utils/feature/index.d.ts +3 -0
  72. package/dist/utils/feature/useFeature.hook.d.ts +15 -0
  73. package/dist/utils/index.d.ts +2 -0
  74. package/dist/utils/json/traverseObject.d.ts +1 -1
  75. package/dist/utils/parseNum.d.ts +1 -0
  76. package/dist/utils/testUtils/index.d.ts +7 -0
  77. package/dist/utils/workerPool.d.ts +1 -1
  78. package/package.json +3 -3
  79. package/src/artifact/ArtifactIconService.ts +1 -0
  80. package/src/artifact/ArtifactTypes.ts +1 -0
  81. package/src/artifact/ExpectedArtifactSelectorViewController.ts +1 -1
  82. package/src/config/VersionChecker.tsx +1 -1
  83. package/src/domain/IServerGroup.ts +1 -0
  84. package/src/help/help.contents.ts +1 -1
  85. package/src/navigation/UrlBuilder.ts +15 -0
  86. package/src/notification/NotificationsList.tsx +12 -8
  87. package/src/pipeline/config/stages/bakeManifest/helm/BakeHelmConfigForm.tsx +8 -2
  88. package/src/pipeline/config/triggers/artifacts/helm-image/HelmImageArtifactEditor.tsx +169 -0
  89. package/src/pipeline/config/triggers/artifacts/index.ts +3 -0
  90. package/src/pipeline/config/validation/PipelineConfigValidator.ts +2 -2
  91. package/src/pipeline/executions/executionGroup/ExecutionGroups.tsx +37 -2
  92. package/src/pipeline/filter/executionFilter.service.ts +50 -5
  93. package/src/presentation/details/Details.tsx +18 -1
  94. package/src/presentation/forms/FormikForm.tsx +1 -1
  95. package/src/projects/ProjectHeader.tsx +12 -9
  96. package/src/projects/Projects.spec.tsx +141 -0
  97. package/src/projects/Projects.tsx +148 -0
  98. package/src/projects/index.ts +1 -0
  99. package/src/projects/{projects.module.js → projects.module.ts} +0 -2
  100. package/src/projects/projects.states.ts +4 -6
  101. package/src/serverGroup/details/ServerGroupDetails.tsx +1 -1
  102. package/src/serverGroupManager/ServerGroupManager.tsx +2 -0
  103. package/src/serverGroupManager/ServerGroupManagerTag.tsx +1 -1
  104. package/src/serverGroupManager/index.ts +1 -0
  105. package/src/serverGroupManager/serverGroupManager.states.ts +3 -3
  106. package/src/utils/feature/Feature.tsx +98 -0
  107. package/src/utils/feature/FeatureContext.tsx +49 -0
  108. package/src/utils/feature/index.ts +3 -0
  109. package/src/utils/feature/useFeature.hook.tsx +25 -0
  110. package/src/utils/index.ts +2 -0
  111. package/src/utils/parseNum.ts +2 -0
  112. package/src/utils/testUtils/index.tsx +30 -0
  113. package/dist/projects/projects.controller.d.ts +0 -2
  114. package/src/projects/projects.controller.js +0 -112
  115. package/src/projects/projects.controller.spec.js +0 -86
  116. package/src/projects/projects.html +0 -87
@@ -10,7 +10,6 @@ import { ConfigureProjectModal } from './configure/ConfigureProjectModal';
10
10
  import type { IProject } from '../domain';
11
11
  import { Overridable } from '../overrideRegistry';
12
12
  import { SpanDropdownTrigger } from '../presentation';
13
- import { ReactInjector } from '../reactShims';
14
13
 
15
14
  import './project.less';
16
15
 
@@ -47,16 +46,20 @@ export class ProjectHeader extends React.Component<IProjectHeaderProps, IProject
47
46
 
48
47
  private configureProject = () => {
49
48
  const { projectConfiguration } = this.props;
50
- const { $state } = ReactInjector;
49
+ const $state = this.props.transition.router.stateService;
51
50
  const title = 'Configure project';
52
51
 
53
- ConfigureProjectModal.show({ title, projectConfiguration }).then((result) => {
54
- if (result.action === 'delete') {
55
- $state.go('home.infrastructure');
56
- } else if (result.action === 'upsert') {
57
- $state.go($state.current, { project: result.name }, { location: 'replace', reload: true });
58
- }
59
- });
52
+ ConfigureProjectModal.show({ title, projectConfiguration })
53
+ .then((result) => {
54
+ if (result.action === 'delete') {
55
+ $state.go('home.infrastructure');
56
+ } else if (result.action === 'upsert') {
57
+ $state.go($state.current, { project: result.name }, { location: 'replace', reload: true });
58
+ }
59
+ })
60
+ .catch((err) => {
61
+ console.error(err);
62
+ });
60
63
  };
61
64
 
62
65
  public render() {
@@ -0,0 +1,141 @@
1
+ import type { UIRouterReact } from '@uirouter/react';
2
+ import { mock } from 'angular';
3
+ import type { ReactWrapper } from 'enzyme';
4
+ import * as React from 'react';
5
+ import { act } from 'react-dom/test-utils';
6
+
7
+ import { Projects } from './Projects';
8
+ import { ViewStateCache } from '../cache';
9
+ import { OVERRIDE_REGISTRY } from '../overrideRegistry';
10
+ import { REACT_MODULE } from '../reactShims';
11
+ import * as ProjectReaderModule from './service/ProjectReader';
12
+ import { timestamp } from '../utils';
13
+ import { mountAndFlush } from '../utils/testUtils';
14
+
15
+ type TestProject = ReturnType<typeof makeProject>;
16
+
17
+ const makeProject = (name: string, email: string, createTs: number, updateTs: number) => ({
18
+ id: name,
19
+ name,
20
+ email,
21
+ createTs,
22
+ updateTs,
23
+ config: { pipelineConfigs: [], applications: [], clusters: [] },
24
+ lastModifiedBy: 'anonymous',
25
+ });
26
+
27
+ const deck = makeProject('deck', 'a@netflix.com', new Date(2).getTime(), new Date(2).getTime());
28
+ const oort = makeProject('oort', 'b@netflix.com', new Date(3).getTime(), new Date(3).getTime());
29
+ const mort = makeProject('mort', 'c@netflix.com', new Date(1).getTime(), new Date(1).getTime());
30
+ const projectList: TestProject[] = [deck, oort, mort];
31
+
32
+ const getRenderedNames = (wrapper: ReactWrapper) => wrapper.find('tbody tr').map((r) => r.find('td a').first().text());
33
+
34
+ export function invokeSort(toggle: ReactWrapper<any>, next: string) {
35
+ const onChange = toggle.prop('onChange') as (v: string) => void;
36
+ onChange(next);
37
+ }
38
+
39
+ describe('Projects', () => {
40
+ let $uiRouter: UIRouterReact;
41
+ let listSpy: jasmine.Spy;
42
+
43
+ beforeEach(mock.module(REACT_MODULE, OVERRIDE_REGISTRY));
44
+ beforeEach(
45
+ mock.inject((_$uiRouter_: UIRouterReact) => {
46
+ $uiRouter = _$uiRouter_;
47
+ }),
48
+ );
49
+
50
+ describe('filtering & sorting', () => {
51
+ beforeEach(() => {
52
+ listSpy = spyOn(ProjectReaderModule.ProjectReader, 'listProjects').and.returnValue(Promise.resolve(projectList));
53
+ });
54
+
55
+ afterEach(() => {
56
+ ViewStateCache.clearCache('projects');
57
+ });
58
+
59
+ it('sets loaded flag and renders projects sorted by name asc', async () => {
60
+ const wrapper = await mountAndFlush(<Projects />);
61
+
62
+ const rows = wrapper.find('tbody tr');
63
+ expect(rows.length).toBe(3);
64
+
65
+ expect(getRenderedNames(wrapper)).toEqual(['deck', 'mort', 'oort']);
66
+
67
+ const firstRowTds = rows.at(0).find('td');
68
+ expect(firstRowTds.at(1).text()).toContain(timestamp(new Date(2).getTime())); // createTs
69
+ expect(firstRowTds.at(2).text()).toContain(timestamp(new Date(2).getTime())); // updateTs
70
+ expect(firstRowTds.at(3).text()).toBe('a@netflix.com');
71
+ });
72
+
73
+ it('filters by name or email as the user types', async () => {
74
+ const wrapper = await mountAndFlush(<Projects />);
75
+
76
+ const input = wrapper.find('input[placeholder="Search projects"]');
77
+ expect(input.exists()).toBeTrue();
78
+
79
+ // Filter by email
80
+ await act(async () => {
81
+ input.prop('onChange')?.({ target: { value: 'a@netflix.com' } } as any);
82
+ });
83
+ wrapper.update();
84
+ let rows = wrapper.find('tbody tr');
85
+ expect(rows.length).toBe(1);
86
+ expect(rows.at(0).find('td a').text()).toBe('deck');
87
+
88
+ // Filter by substring 'ort'
89
+ await act(async () => {
90
+ input.prop('onChange')?.({ target: { value: 'ort' } } as any);
91
+ });
92
+ wrapper.update();
93
+ rows = wrapper.find('tbody tr');
94
+ expect(rows.map((r) => r.find('td a').text())).toEqual(['mort', 'oort']);
95
+
96
+ // Clear
97
+ await act(async () => {
98
+ input.prop('onChange')?.({ target: { value: '' } } as any);
99
+ });
100
+ wrapper.update();
101
+ expect(wrapper.find('tbody tr').length).toBe(3);
102
+ });
103
+
104
+ it('sorts by -name, -createTs, createTs, and combines with a filter', async () => {
105
+ const wrapper = await mountAndFlush(<Projects />);
106
+
107
+ const sortToggles = wrapper.find('SortToggle');
108
+
109
+ // -name (desc)
110
+ const nameToggle = sortToggles.filterWhere((n) => n.prop('label') === 'Name').first();
111
+ await act(async () => {
112
+ invokeSort(nameToggle, '-name');
113
+ });
114
+ wrapper.update();
115
+ expect(getRenderedNames(wrapper)).toEqual(['oort', 'mort', 'deck']);
116
+
117
+ // -createTs (desc)
118
+ const createdToggle = sortToggles.filterWhere((n) => n.prop('label') === 'Created').first();
119
+ await act(async () => {
120
+ invokeSort(createdToggle, '-createTs');
121
+ });
122
+ wrapper.update();
123
+ expect(getRenderedNames(wrapper)).toEqual(['oort', 'deck', 'mort']);
124
+
125
+ // -createTs (asc)
126
+ await act(async () => {
127
+ invokeSort(createdToggle, 'createTs');
128
+ });
129
+ wrapper.update();
130
+ expect(getRenderedNames(wrapper)).toEqual(['mort', 'deck', 'oort']);
131
+
132
+ // Add filter ("ort") while sorted by createTs
133
+ const input = wrapper.find('input[placeholder="Search projects"]');
134
+ await act(async () => {
135
+ input.prop('onChange')?.({ target: { value: 'ort' } } as any);
136
+ });
137
+ wrapper.update();
138
+ expect(getRenderedNames(wrapper)).toEqual(['mort', 'oort']);
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,148 @@
1
+ import { UISref } from '@uirouter/react';
2
+ import { orderBy } from 'lodash';
3
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+ import type { SelectCallback } from 'react-bootstrap';
5
+
6
+ import { PaginationControls } from '../application/search/PaginationControls';
7
+ import { ViewStateCache } from '../cache';
8
+ import type { IProject } from '../domain';
9
+ import { InsightMenu } from '../insight/InsightMenu';
10
+ import { SortToggle } from '../presentation';
11
+ import { anyFieldFilter } from '../presentation/anyFieldFilter/anyField.filter';
12
+ import { ProjectReader } from './service/ProjectReader';
13
+ import { timestamp } from '../utils';
14
+ import { Spinner } from '../widgets';
15
+
16
+ interface IProjectSummary extends IProject {
17
+ createTs?: number;
18
+ updateTs?: number;
19
+ email: string;
20
+ }
21
+
22
+ interface IProjectsViewState {
23
+ projectFilter: string;
24
+ sort: string;
25
+ }
26
+
27
+ export const Projects = () => {
28
+ const cache = useRef(ViewStateCache.get('projects') || ViewStateCache.createCache('projects', { version: 1 }))
29
+ .current;
30
+ const cached: IProjectsViewState = cache.get('#global') || { projectFilter: '', sort: 'name' };
31
+
32
+ const [projects, setProjects] = useState<IProjectSummary[]>([]);
33
+ const [loaded, setLoaded] = useState(false);
34
+ const [projectFilter, setProjectFilter] = useState<string>(cached.projectFilter);
35
+ const [sortKey, setSortKey] = useState<string>(cached.sort);
36
+ const [currentPage, setCurrentPage] = useState(1);
37
+ const inputRef = useRef<HTMLInputElement>();
38
+
39
+ useEffect(() => {
40
+ cache.put('#global', { projectFilter, sort: sortKey });
41
+ }, [projectFilter, sortKey, cache]);
42
+
43
+ useEffect(() => {
44
+ ProjectReader.listProjects().then((result: IProjectSummary[]) => {
45
+ setProjects(result);
46
+ setLoaded(true);
47
+ });
48
+ }, []);
49
+
50
+ useEffect(() => {
51
+ inputRef.current?.focus();
52
+ }, []);
53
+
54
+ useEffect(() => {
55
+ setCurrentPage(1);
56
+ }, [projectFilter, sortKey]);
57
+
58
+ const filteredProjects = useMemo(() => {
59
+ const filterFn = anyFieldFilter();
60
+ const filtered = filterFn(projects, {
61
+ name: projectFilter,
62
+ email: projectFilter,
63
+ });
64
+ const key = sortKey?.startsWith('-') ? sortKey.slice(1) : sortKey;
65
+ const direction = sortKey?.startsWith('-') ? 'desc' : 'asc';
66
+ return orderBy(filtered, [key], [direction]);
67
+ }, [projects, projectFilter, sortKey]);
68
+
69
+ const itemsPerPage = 12;
70
+ const totalPages = Math.ceil(filteredProjects.length / itemsPerPage) || 1;
71
+ const pageProjects = filteredProjects.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
72
+
73
+ const changePage: SelectCallback = (page: any) => setCurrentPage(Number(page));
74
+
75
+ const Loading = () => (
76
+ <div className="horizontal center middle" style={{ marginBottom: '250px', height: '100px' }}>
77
+ <Spinner size="small" />
78
+ </div>
79
+ );
80
+
81
+ return (
82
+ <div className="infrastructure">
83
+ <div className="infrastructure-section search-header">
84
+ <div className="container">
85
+ <h2 className="header-section">
86
+ <span className="search-label">Projects</span>
87
+ <input
88
+ type="search"
89
+ placeholder="Search projects"
90
+ className="form-control input-md"
91
+ ref={inputRef}
92
+ value={projectFilter}
93
+ onChange={(e) => setProjectFilter(e.target.value)}
94
+ />
95
+ </h2>
96
+ <div className="header-actions">
97
+ <InsightMenu createApp={false} createProject={true} refreshCaches={false} />
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <div className="container">
102
+ {!loaded && <Loading />}
103
+ {loaded && (
104
+ <>
105
+ <table className="table table-hover">
106
+ <thead>
107
+ <tr>
108
+ <th style={{ width: '20%' }}>
109
+ <SortToggle currentSort={sortKey} onChange={setSortKey} label="Name" sortKey="name" />
110
+ </th>
111
+ <th style={{ width: '20%' }}>
112
+ <SortToggle currentSort={sortKey} onChange={setSortKey} label="Created" sortKey="createTs" />
113
+ </th>
114
+ <th style={{ width: '20%' }}>
115
+ <SortToggle currentSort={sortKey} onChange={setSortKey} label="Updated" sortKey="updateTs" />
116
+ </th>
117
+ <th style={{ width: '25%' }}>
118
+ <SortToggle currentSort={sortKey} onChange={setSortKey} label="Owner" sortKey="email" />
119
+ </th>
120
+ </tr>
121
+ </thead>
122
+ <tbody>
123
+ {pageProjects.map((project) => {
124
+ const projectName = project.name.toLowerCase();
125
+ return (
126
+ <UISref key={projectName} to="home.project.dashboard" params={{ project: projectName }}>
127
+ <tr className="clickable">
128
+ <td>
129
+ <UISref to="home.project.dashboard" params={{ project: projectName }}>
130
+ <a>{projectName}</a>
131
+ </UISref>
132
+ </td>
133
+ <td>{timestamp(project.createTs)}</td>
134
+ <td>{timestamp(project.updateTs)}</td>
135
+ <td>{project.email}</td>
136
+ </tr>
137
+ </UISref>
138
+ );
139
+ })}
140
+ </tbody>
141
+ </table>
142
+ <PaginationControls onPageChanged={changePage} activePage={currentPage} totalPages={totalPages} />
143
+ </>
144
+ )}
145
+ </div>
146
+ </div>
147
+ );
148
+ };
@@ -3,3 +3,4 @@ export * from './ProjectHeader';
3
3
  export * from './projectSearchResultType';
4
4
  export * from './service/ProjectReader';
5
5
  export * from './service/ProjectWriter';
6
+ export * from './Projects';
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  import { module } from 'angular';
4
2
 
5
3
  import './projectSearchResultType';
@@ -2,13 +2,13 @@ import type { StateParams } from '@uirouter/angularjs';
2
2
  import { module } from 'angular';
3
3
 
4
4
  import { ProjectHeader } from './ProjectHeader';
5
+ import { Projects } from './Projects';
5
6
  import type { ApplicationStateProvider } from '../application/application.state.provider';
6
7
  import { APPLICATION_STATE_PROVIDER } from '../application/application.state.provider';
7
8
  import { CORE_PROJECTS_DASHBOARD_DASHBOARD_CONTROLLER } from './dashboard/dashboard.controller';
8
9
  import type { IProject } from '../domain/IProject';
9
10
  import type { INestedState, StateConfigProvider } from '../navigation/state.provider';
10
11
  import { STATE_CONFIG_PROVIDER } from '../navigation/state.provider';
11
- import { CORE_PROJECTS_PROJECTS_CONTROLLER } from './projects.controller';
12
12
  import { ProjectReader } from './service/ProjectReader';
13
13
 
14
14
  export interface IProjectStateParms extends StateParams {
@@ -17,7 +17,6 @@ export interface IProjectStateParms extends StateParams {
17
17
 
18
18
  export const PROJECTS_STATES_CONFIG = 'spinnaker.core.projects.state.config';
19
19
  module(PROJECTS_STATES_CONFIG, [
20
- CORE_PROJECTS_PROJECTS_CONTROLLER,
21
20
  CORE_PROJECTS_DASHBOARD_DASHBOARD_CONTROLLER,
22
21
  APPLICATION_STATE_PROVIDER,
23
22
  STATE_CONFIG_PROVIDER,
@@ -58,7 +57,7 @@ module(PROJECTS_STATES_CONFIG, [
58
57
  email: null,
59
58
  config: null,
60
59
  notFound: true,
61
- };
60
+ } as IProject;
62
61
  },
63
62
  );
64
63
  },
@@ -88,9 +87,8 @@ module(PROJECTS_STATES_CONFIG, [
88
87
  url: '/projects',
89
88
  views: {
90
89
  'main@': {
91
- templateUrl: require('../projects/projects.html'),
92
- controller: 'ProjectsCtrl',
93
- controllerAs: 'ctrl',
90
+ component: Projects,
91
+ $type: 'react',
94
92
  },
95
93
  },
96
94
  data: {
@@ -97,7 +97,7 @@ export class ServerGroupDetails extends React.Component<IServerGroupDetailsProps
97
97
  <div className="header-text horizontal middle">
98
98
  <CloudProviderLogo provider={serverGroup.type} height="36px" width="36px" />
99
99
  <h3 className="horizontal middle space-between flex-1">
100
- {serverGroup.name}
100
+ {serverGroup.displayName ? serverGroup.displayName : serverGroup.name}
101
101
  {showEntityTags && (
102
102
  <EntityNotifications
103
103
  entity={serverGroup}
@@ -25,6 +25,7 @@ export class ServerGroupManager extends React.Component<IServerGroupManagerProps
25
25
  region: serverGroups[0].region,
26
26
  provider: serverGroups[0].cloudProvider,
27
27
  serverGroupManager: manager,
28
+ name: manager,
28
29
  };
29
30
  return ReactInjector.$state.includes('**.serverGroupManager', params);
30
31
  };
@@ -42,6 +43,7 @@ export class ServerGroupManager extends React.Component<IServerGroupManagerProps
42
43
  region: serverGroups[0].region,
43
44
  provider: serverGroups[0].cloudProvider,
44
45
  serverGroupManager: manager,
46
+ name: manager,
45
47
  });
46
48
  };
47
49
 
@@ -59,7 +59,7 @@ export class ServerGroupManagerTag extends React.Component<IServerGroupManagerTa
59
59
  accountId: serverGroupManager.account,
60
60
  region: serverGroupManager.region,
61
61
  provider: serverGroupManager.cloudProvider,
62
- serverGroupManager: serverGroupManager.name,
62
+ name: serverGroupManager.name,
63
63
  };
64
64
  }
65
65
  }
@@ -2,3 +2,4 @@ export * from './serverGroupManager.dataSource';
2
2
  export * from './serverGroupManager.states';
3
3
  export * from './ServerGroupManagerReader';
4
4
  export * from './ServerGroupManagerTag';
5
+ export * from './ServerGroupManagerDetails';
@@ -9,7 +9,7 @@ export interface IServerGroupManagerStateParams {
9
9
  provider: string;
10
10
  accountId: string;
11
11
  region: string;
12
- serverGroupManager: string;
12
+ name: string;
13
13
  }
14
14
 
15
15
  export const SERVER_GROUP_MANAGER_STATES = 'spinnaker.core.serverGroupManager.states';
@@ -18,7 +18,7 @@ module(SERVER_GROUP_MANAGER_STATES, [APPLICATION_STATE_PROVIDER]).config([
18
18
  (applicationStateProvider: ApplicationStateProvider) => {
19
19
  const serverGroupManagerDetails: INestedState = {
20
20
  name: 'serverGroupManager',
21
- url: '/serverGroupManagerDetails/:provider/:accountId/:region/:serverGroupManager',
21
+ url: '/serverGroupManagerDetails/:provider/:accountId/:region/:name',
22
22
  views: {
23
23
  'detail@../insight': {
24
24
  component: ServerGroupManagerDetails,
@@ -32,7 +32,7 @@ module(SERVER_GROUP_MANAGER_STATES, [APPLICATION_STATE_PROVIDER]).config([
32
32
  data: {
33
33
  pageTitleDetails: {
34
34
  title: 'Server Group Manager Details',
35
- nameParam: 'serverGroupManager',
35
+ nameParam: 'name',
36
36
  accountParam: 'accountId',
37
37
  regionParam: 'region',
38
38
  },
@@ -0,0 +1,98 @@
1
+ import * as React from 'react';
2
+
3
+ import type { FeatureFlags } from './FeatureContext';
4
+ import { useFeature } from './useFeature.hook';
5
+
6
+ interface FeatureProps {
7
+ feature: keyof FeatureFlags;
8
+ children?: React.ReactNode;
9
+ render?: React.ReactNode;
10
+ }
11
+
12
+ /**
13
+ * Renders content conditionally based on the specified feature flag.
14
+ *
15
+ * @param {Object} props - The properties for the Feature component.
16
+ * @param {keyof FeatureFlags} props.feature - The name of the feature flag to check.
17
+ * @param {React.ReactNode} [props.children] - The content to render if the feature is enabled.
18
+ * @param {React.ReactNode} [props.render=children] - Optional custom render content; defaults to `children`.
19
+ *
20
+ * @returns {JSX.Element | null} - The rendered content if the feature is enabled; otherwise, `null`.
21
+ *
22
+ * @example
23
+ * // Example usage with children
24
+ * <Feature feature="slack">
25
+ * <SlackComponent />
26
+ * </Feature>
27
+ *
28
+ * @example
29
+ * // Example usage with a custom render prop
30
+ * <Feature
31
+ * feature="entityTags"
32
+ * render={<EntityTagsComponent />}
33
+ * />
34
+ */
35
+ export function Feature({ feature, children, render = children }: FeatureProps): JSX.Element | null {
36
+ const hasFeature = useFeature(feature);
37
+
38
+ // If the feature is disabled, render nothing.
39
+ if (!hasFeature) {
40
+ return null;
41
+ }
42
+
43
+ // Otherwise, render the node content.
44
+ return <React.Fragment>{render}</React.Fragment>;
45
+ }
46
+
47
+ /**
48
+ * An alias of the `<Feature />` component.
49
+ * Renders the provided content if the specified feature is enabled.
50
+ *
51
+ * @example
52
+ * <IfFeatureEnabled feature="slack">
53
+ * <SlackComponent />
54
+ * </IfFeatureEnabled>
55
+ */
56
+ export function IfFeatureEnabled({ feature, children, render = children }: FeatureProps): JSX.Element | null {
57
+ return <Feature feature={feature} render={render} />;
58
+ }
59
+
60
+ /**
61
+ * Higher-order function to wrap a component with a feature flag check.
62
+ * Ensures that the component only renders if the specified feature is enabled.
63
+ *
64
+ * @template Props
65
+ * @param {keyof FeatureFlags} feature - The name of the feature flag to check.
66
+ * @returns {(Component: React.ComponentType<Props>) => React.ComponentType<Props>} -
67
+ * A higher-order component (HOC) that conditionally renders the wrapped component.
68
+ *
69
+ * @example
70
+ * // Example usage
71
+ * const EnhancedComponent = withFeature('newFeature')(MyComponent);
72
+ *
73
+ * <EnhancedComponent someProp="value" />;
74
+ */
75
+ export function withFeature<Props extends object>(
76
+ feature: keyof FeatureFlags,
77
+ ): (Component: React.ComponentType<Props>) => React.ComponentType<Props> {
78
+ return function wrapWithFeature(Component: React.ComponentType<Props>) {
79
+ /**
80
+ * Wraps the provided component with the <Feature> component,
81
+ * checking the specified feature flag before rendering.
82
+ *
83
+ * @param {Props} props - Props passed to the wrapped component.
84
+ * @returns {JSX.Element} - The wrapped component if the feature is enabled, or nothing if it is disabled.
85
+ */
86
+ function WithFeature(props: Props): JSX.Element {
87
+ return (
88
+ <Feature feature={feature}>
89
+ <Component {...props} />
90
+ </Feature>
91
+ );
92
+ }
93
+
94
+ WithFeature.displayName = `WithFeature(${Component.displayName || Component.name})`;
95
+
96
+ return WithFeature;
97
+ };
98
+ }
@@ -0,0 +1,49 @@
1
+ import * as React from 'react';
2
+
3
+ import type { IFeatures } from '../../config';
4
+ import { SETTINGS } from '../../config';
5
+ import { useFeatures } from './useFeature.hook';
6
+
7
+ /**
8
+ * Type alias for feature flags, extending from IFeatures interface.
9
+ */
10
+ export type FeatureFlags = IFeatures;
11
+
12
+ /**
13
+ * Creates a React Context for feature flags.
14
+ * Defaults to `SETTINGS.feature` if no provider is found in the component tree.
15
+ */
16
+ export const FeatureFlagsContext = React.createContext<FeatureFlags>(SETTINGS.feature);
17
+
18
+ /**
19
+ * Merges two FeatureFlags objects. Flags in `b` will override flags in `a`.
20
+ *
21
+ * @param {FeatureFlags} a - The base feature flags.
22
+ * @param {FeatureFlags} b - The overriding feature flags.
23
+ * @returns {FeatureFlags} - A new object containing merged flags from `a` and `b`.
24
+ */
25
+ function mergeFeatures(a: FeatureFlags, b: FeatureFlags): FeatureFlags {
26
+ return { ...a, ...b };
27
+ }
28
+
29
+ /**
30
+ * A context provider component that merges the global feature flags
31
+ * from `SETTINGS.feature` with any feature flags passed in via props.
32
+ *
33
+ * @param {Object} props
34
+ * @param {FeatureFlags} [props.features] - Feature flags to merge with the global flags.
35
+ * @param {React.ReactNode} props.children - Components that will consume the merged flags.
36
+ * @returns {JSX.Element} - The provider that holds merged feature flags in context.
37
+ */
38
+ export function FeaturesProvider({
39
+ features = {},
40
+ children,
41
+ }: {
42
+ features?: FeatureFlags;
43
+ children: React.ReactNode;
44
+ }): JSX.Element {
45
+ const currentFeatures = useFeatures();
46
+ // Merge global settings with any flags provided to the provider
47
+ const mergedFeatures = React.useMemo(() => mergeFeatures(currentFeatures, features), [features]);
48
+ return <FeatureFlagsContext.Provider value={mergedFeatures}>{children}</FeatureFlagsContext.Provider>;
49
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Feature';
2
+ export * from './FeatureContext';
3
+ export * from './useFeature.hook';
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+
3
+ import type { FeatureFlags } from './FeatureContext';
4
+ import { FeatureFlagsContext } from './FeatureContext';
5
+
6
+ /**
7
+ * A hook to access the current set of feature flags from the context.
8
+ * If no provider is found, it will default to `SETTINGS.feature`.
9
+ *
10
+ * @returns {FeatureFlags} - The feature flags currently in the context.
11
+ */
12
+ export function useFeatures(): FeatureFlags {
13
+ return React.useContext(FeatureFlagsContext);
14
+ }
15
+
16
+ /**
17
+ * A hook to determine if a particular feature is enabled.
18
+ *
19
+ * @param {keyof FeatureFlags} feature - The name of the feature flag to check.
20
+ * @returns {boolean} - True if the feature is enabled; otherwise false.
21
+ */
22
+ export function useFeature(feature: keyof FeatureFlags): boolean {
23
+ const features = useFeatures();
24
+ return features[feature];
25
+ }
@@ -13,6 +13,7 @@ export * from './json/traverseObject';
13
13
  export * from './noop';
14
14
  export * from './q';
15
15
  export * from './renderIfFeature.component';
16
+ export * from './feature';
16
17
  export * from './scrollTo/scrollTo.service';
17
18
  export * from './timeFormatters';
18
19
  export * from './unicodeBase64';
@@ -20,3 +21,4 @@ export * from './uuid.service';
20
21
  export * from './workerPool';
21
22
  export * from './json/filterObjectValues';
22
23
  export * from './Logger';
24
+ export * from './parseNum';
@@ -0,0 +1,2 @@
1
+ export const parseNum = (numOrStr: number | string): number =>
2
+ typeof numOrStr === 'string' ? parseInt(numOrStr) : numOrStr;