@spinnaker/core 2025.1.4 → 2025.2.1
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/dist/domain/ILoadBalancer.d.ts +1 -0
- package/dist/index.js +286 -177
- package/dist/index.js.map +1 -1
- package/dist/loadBalancer/details/LoadBalancerDetails.d.ts +3 -0
- package/dist/loadBalancer/details/LoadBalancerDetailsWrapper.d.ts +37 -0
- package/dist/loadBalancer/details/index.d.ts +2 -0
- package/dist/loadBalancer/index.d.ts +1 -0
- package/dist/loadBalancer/loadBalancer.states.d.ts +7 -0
- package/dist/presentation/modal/index.d.ts +1 -0
- package/dist/presentation/modal/useModal.d.ts +30 -0
- package/dist/task/index.d.ts +1 -0
- package/dist/task/monitor/useTaskMonitor.d.ts +25 -0
- package/package.json +2 -2
- package/src/domain/ILoadBalancer.ts +1 -0
- package/src/loadBalancer/details/LoadBalancerDetails.tsx +64 -0
- package/src/loadBalancer/details/LoadBalancerDetailsWrapper.tsx +120 -0
- package/src/loadBalancer/details/index.ts +2 -0
- package/src/loadBalancer/index.ts +1 -0
- package/src/loadBalancer/loadBalancer.states.ts +11 -2
- package/src/manifest/stage/JobStageExecutionLogs.spec.tsx +208 -0
- package/src/manifest/stage/JobStageExecutionLogs.tsx +2 -2
- package/src/presentation/modal/index.ts +1 -0
- package/src/presentation/modal/useModal.ts +45 -0
- package/src/task/index.ts +1 -0
- package/src/task/monitor/useTaskMonitor.ts +30 -0
- package/dist/loadBalancer/LoadBalancerDetails.d.ts +0 -7
- package/src/loadBalancer/LoadBalancerDetails.tsx +0 -13
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FunctionComponent } from 'react';
|
|
2
|
+
import type { Application } from '../../application';
|
|
3
|
+
import type { ILoadBalancer } from '../../domain';
|
|
4
|
+
import type { ILoadBalancerStateParams } from '../loadBalancer.states';
|
|
5
|
+
import type { IOverridableProps } from '../../overrideRegistry';
|
|
6
|
+
export interface ILoadBalancerDetailsWrapperProps extends IOverridableProps {
|
|
7
|
+
app: Application;
|
|
8
|
+
loadBalancer: ILoadBalancerStateParams;
|
|
9
|
+
}
|
|
10
|
+
export interface UseDetailsResult<T> {
|
|
11
|
+
data: T | undefined;
|
|
12
|
+
loading: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
refetch: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface IUseDetailsHookProps {
|
|
17
|
+
app: Application;
|
|
18
|
+
loadBalancerParams: ILoadBalancerStateParams;
|
|
19
|
+
autoClose: () => void;
|
|
20
|
+
}
|
|
21
|
+
export type UseDetailsHook<T> = (props: IUseDetailsHookProps) => UseDetailsResult<T>;
|
|
22
|
+
export interface ILoadBalancerDetailsSectionProps {
|
|
23
|
+
app: Application;
|
|
24
|
+
loadBalancer: ILoadBalancer;
|
|
25
|
+
}
|
|
26
|
+
export interface ILoadBalancerActionsProps {
|
|
27
|
+
app: Application;
|
|
28
|
+
loadBalancer: ILoadBalancer;
|
|
29
|
+
}
|
|
30
|
+
export interface ILoadBalancerDetailsProps extends ILoadBalancerDetailsWrapperProps {
|
|
31
|
+
useDetails: UseDetailsHook<ILoadBalancer>;
|
|
32
|
+
Actions: FunctionComponent<ILoadBalancerActionsProps>;
|
|
33
|
+
sections: Array<FunctionComponent<ILoadBalancerDetailsSectionProps>>;
|
|
34
|
+
}
|
|
35
|
+
declare function LoadBalancerDetailsWrapper({ app, loadBalancer }: ILoadBalancerDetailsWrapperProps): JSX.Element;
|
|
36
|
+
export declare const LoadBalancerDetails: typeof LoadBalancerDetailsWrapper;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom hook to manage modal visibility.
|
|
3
|
+
* Provides methods to open and close a modal.
|
|
4
|
+
*
|
|
5
|
+
* @param {UseModalProps} [props] - Configuration options for initial modal state.
|
|
6
|
+
* @param {boolean} [props.defaultOpen=false] - Initial visibility state of the modal.
|
|
7
|
+
* @returns {UseModalReturn} - The current state and control methods for the modal.
|
|
8
|
+
*/
|
|
9
|
+
export type UseModalReturn = {
|
|
10
|
+
open: boolean;
|
|
11
|
+
show: () => void;
|
|
12
|
+
close: () => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Configuration options for the useModal hook.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {Object} UseModalProps
|
|
18
|
+
* @property {boolean} [defaultOpen=false] - Initial state of the modal.
|
|
19
|
+
*/
|
|
20
|
+
export type UseModalProps = {
|
|
21
|
+
defaultOpen?: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Hook to control modal visibility.
|
|
25
|
+
* Returns methods to open and close a modal, and the modal's current state.
|
|
26
|
+
*
|
|
27
|
+
* @param {UseModalProps} [props] - The configuration for the modal.
|
|
28
|
+
* @returns {UseModalReturn} - Modal visibility state and control methods.
|
|
29
|
+
*/
|
|
30
|
+
export declare const useModal: ({ defaultOpen }?: UseModalProps) => UseModalReturn;
|
package/dist/task/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './PlatformHealthOverrideMessage';
|
|
2
2
|
export * from './monitor/TaskMonitor';
|
|
3
3
|
export * from './monitor/TaskMonitorWrapper';
|
|
4
|
+
export * from './monitor/useTaskMonitor';
|
|
4
5
|
export * from './modal/TaskReason';
|
|
5
6
|
export * from './modal/TaskMonitorModal';
|
|
6
7
|
export * from './task.read.service';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ITaskMonitorConfig } from './TaskMonitor';
|
|
2
|
+
import { TaskMonitor } from './TaskMonitor';
|
|
3
|
+
/**
|
|
4
|
+
* React hook that returns a TaskMonitor
|
|
5
|
+
*
|
|
6
|
+
* @param config a ITaskMonitorConfig
|
|
7
|
+
* @param dismissModal a function that closes the modal enclosing the task monitor
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
*
|
|
11
|
+
* function MyComponent(props) {
|
|
12
|
+
* const { application, serverGroup } = props;
|
|
13
|
+
* const title = `Resize ${serverGroup.name}`;
|
|
14
|
+
* const taskMonitor = useTaskMonitor({ application, title });
|
|
15
|
+
*
|
|
16
|
+
* return (
|
|
17
|
+
* <>
|
|
18
|
+
* <TaskMonitorWrapper taskMonitor={taskMonitor}>
|
|
19
|
+
* <form onSubmit={() => taskMonitor.submit(() => API.runSomeTask())}>
|
|
20
|
+
* </>
|
|
21
|
+
* )
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
export declare const useTaskMonitor: (config: ITaskMonitorConfig, dismissModal: () => void) => TaskMonitor;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spinnaker/core",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2025.1
|
|
4
|
+
"version": "2025.2.1",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
@@ -124,5 +124,5 @@
|
|
|
124
124
|
"shx": "0.3.3",
|
|
125
125
|
"typescript": "5.0.4"
|
|
126
126
|
},
|
|
127
|
-
"gitHead": "
|
|
127
|
+
"gitHead": "33bad18275d63c4000a8e757cc0b4816e2e7eed5"
|
|
128
128
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useRouter } from '@uirouter/react';
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { ILoadBalancerDetailsProps } from './LoadBalancerDetailsWrapper';
|
|
5
|
+
import { CloudProviderLogo } from '../../cloudProvider';
|
|
6
|
+
import { EntityNotifications } from '../../entityTag/notifications/EntityNotifications';
|
|
7
|
+
import { Details } from '../../presentation';
|
|
8
|
+
import { IfFeatureEnabled } from '../../utils';
|
|
9
|
+
|
|
10
|
+
export function LoadBalancerDetailsContent({
|
|
11
|
+
app,
|
|
12
|
+
loadBalancer: params,
|
|
13
|
+
useDetails,
|
|
14
|
+
Actions,
|
|
15
|
+
sections,
|
|
16
|
+
}: ILoadBalancerDetailsProps) {
|
|
17
|
+
const [initialized, setInitialized] = useState(false);
|
|
18
|
+
const {
|
|
19
|
+
stateService: { go },
|
|
20
|
+
} = useRouter();
|
|
21
|
+
const autoClose = () => {
|
|
22
|
+
go('^', { allowModalToStayOpen: true }, { location: 'replace' });
|
|
23
|
+
};
|
|
24
|
+
const { data: loadBalancer, loading } = useDetails({ app, loadBalancerParams: params, autoClose });
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (loadBalancer) {
|
|
28
|
+
setInitialized(true);
|
|
29
|
+
}
|
|
30
|
+
}, [loading]);
|
|
31
|
+
|
|
32
|
+
if (!initialized) return <Details loading={loading} />;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Details loading={loading}>
|
|
36
|
+
<Details.Header
|
|
37
|
+
icon={<CloudProviderLogo provider={params.provider} height="36px" width="36px" />}
|
|
38
|
+
name={loadBalancer.displayName ? loadBalancer.displayName : loadBalancer.name}
|
|
39
|
+
notifications={
|
|
40
|
+
<IfFeatureEnabled
|
|
41
|
+
feature="entityTags"
|
|
42
|
+
render={
|
|
43
|
+
<EntityNotifications
|
|
44
|
+
entity={loadBalancer}
|
|
45
|
+
application={app}
|
|
46
|
+
placement="bottom"
|
|
47
|
+
hOffsetPercent="90%"
|
|
48
|
+
entityType="loadBalancer"
|
|
49
|
+
pageLocation="details"
|
|
50
|
+
onUpdate={() => app.loadBalancers.refresh()}
|
|
51
|
+
/>
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
}
|
|
55
|
+
actions={<Actions key="actions" app={app} loadBalancer={loadBalancer} />}
|
|
56
|
+
/>
|
|
57
|
+
<Details.Content loading={loading}>
|
|
58
|
+
{sections.map((Section, index) => (
|
|
59
|
+
<Section key={index} app={app} loadBalancer={loadBalancer} />
|
|
60
|
+
))}
|
|
61
|
+
</Details.Content>
|
|
62
|
+
</Details>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { $templateCache } from 'ngimport';
|
|
2
|
+
import type { FunctionComponent } from 'react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { LoadBalancerDetailsContent } from './LoadBalancerDetails';
|
|
6
|
+
import type { Application } from '../../application';
|
|
7
|
+
import { CloudProviderRegistry } from '../../cloudProvider';
|
|
8
|
+
import type { ILoadBalancer } from '../../domain';
|
|
9
|
+
import type { ILoadBalancerStateParams } from '../loadBalancer.states';
|
|
10
|
+
import type { IOverridableProps } from '../../overrideRegistry';
|
|
11
|
+
import { overridableComponent } from '../../overrideRegistry';
|
|
12
|
+
import { useData } from '../../presentation';
|
|
13
|
+
import { AngularJSAdapter } from '../../reactShims';
|
|
14
|
+
|
|
15
|
+
export interface ILoadBalancerDetailsWrapperProps extends IOverridableProps {
|
|
16
|
+
app: Application;
|
|
17
|
+
loadBalancer: ILoadBalancerStateParams;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseDetailsResult<T> {
|
|
21
|
+
data: T | undefined;
|
|
22
|
+
loading: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
refetch: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IUseDetailsHookProps {
|
|
28
|
+
app: Application;
|
|
29
|
+
loadBalancerParams: ILoadBalancerStateParams;
|
|
30
|
+
autoClose: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type UseDetailsHook<T> = (props: IUseDetailsHookProps) => UseDetailsResult<T>;
|
|
34
|
+
|
|
35
|
+
export interface ILoadBalancerDetailsSectionProps {
|
|
36
|
+
app: Application;
|
|
37
|
+
loadBalancer: ILoadBalancer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ILoadBalancerActionsProps {
|
|
41
|
+
app: Application;
|
|
42
|
+
loadBalancer: ILoadBalancer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ILoadBalancerDetailsProps extends ILoadBalancerDetailsWrapperProps {
|
|
46
|
+
useDetails: UseDetailsHook<ILoadBalancer>;
|
|
47
|
+
Actions: FunctionComponent<ILoadBalancerActionsProps>;
|
|
48
|
+
sections: Array<FunctionComponent<ILoadBalancerDetailsSectionProps>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface IDetailsTemplateState {
|
|
52
|
+
detailsTemplateUrl?: string;
|
|
53
|
+
detailsController?: string;
|
|
54
|
+
useDetailsHook?: UseDetailsHook<ILoadBalancer>;
|
|
55
|
+
detailsActions?: FunctionComponent<ILoadBalancerActionsProps>;
|
|
56
|
+
detailsSections?: Array<FunctionComponent<ILoadBalancerDetailsSectionProps>>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const getDetailsTemplate = (provider: string): Promise<IDetailsTemplateState> =>
|
|
60
|
+
Promise.all([
|
|
61
|
+
CloudProviderRegistry.getValue(provider, 'loadBalancer.useDetailsHook'),
|
|
62
|
+
CloudProviderRegistry.getValue(provider, 'loadBalancer.detailsActions'),
|
|
63
|
+
CloudProviderRegistry.getValue(provider, 'loadBalancer.detailsSections'),
|
|
64
|
+
CloudProviderRegistry.getValue(provider, 'loadBalancer.detailsTemplateUrl'),
|
|
65
|
+
CloudProviderRegistry.getValue(provider, 'loadBalancer.detailsController'),
|
|
66
|
+
]).then(([useDetailsHook, detailsActions, detailsSections, templateUrl, detailsController]) => {
|
|
67
|
+
const detailsTemplateUrl = templateUrl ? $templateCache.get<string>(templateUrl) : undefined;
|
|
68
|
+
return {
|
|
69
|
+
useDetailsHook,
|
|
70
|
+
detailsActions,
|
|
71
|
+
detailsSections,
|
|
72
|
+
detailsTemplateUrl,
|
|
73
|
+
detailsController,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function LoadBalancerDetailsWrapper({ app, loadBalancer }: ILoadBalancerDetailsWrapperProps) {
|
|
78
|
+
const { provider } = loadBalancer;
|
|
79
|
+
|
|
80
|
+
const { result: detailsTemplate } = useData<IDetailsTemplateState>(() => getDetailsTemplate(provider), {}, [
|
|
81
|
+
provider,
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const {
|
|
85
|
+
useDetailsHook,
|
|
86
|
+
detailsActions: DetailsActions,
|
|
87
|
+
detailsSections,
|
|
88
|
+
detailsTemplateUrl,
|
|
89
|
+
detailsController,
|
|
90
|
+
} = detailsTemplate;
|
|
91
|
+
|
|
92
|
+
if (useDetailsHook && DetailsActions && detailsSections) {
|
|
93
|
+
// React rendering
|
|
94
|
+
return (
|
|
95
|
+
<LoadBalancerDetailsContent
|
|
96
|
+
app={app}
|
|
97
|
+
loadBalancer={loadBalancer}
|
|
98
|
+
useDetails={useDetailsHook}
|
|
99
|
+
Actions={DetailsActions}
|
|
100
|
+
sections={detailsSections}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (detailsTemplateUrl && detailsController) {
|
|
106
|
+
// Angular rendering
|
|
107
|
+
return (
|
|
108
|
+
<AngularJSAdapter
|
|
109
|
+
className="detail-content flex-container-h"
|
|
110
|
+
template={detailsTemplateUrl}
|
|
111
|
+
controller={`${detailsController} as ctrl`}
|
|
112
|
+
locals={{ app, loadBalancer }}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const LoadBalancerDetails = overridableComponent(LoadBalancerDetailsWrapper, 'loadBalancer.details');
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { StateParams } from '@uirouter/angularjs';
|
|
2
2
|
import { module } from 'angular';
|
|
3
3
|
|
|
4
|
-
import { LoadBalancerDetails } from './LoadBalancerDetails';
|
|
5
4
|
import { LoadBalancers } from './LoadBalancers';
|
|
6
5
|
import type { ApplicationStateProvider } from '../application';
|
|
7
6
|
import { APPLICATION_STATE_PROVIDER } from '../application';
|
|
7
|
+
import { LoadBalancerDetails } from './details';
|
|
8
8
|
import { filterModelConfig } from './filter/LoadBalancerFilterModel';
|
|
9
9
|
import { LoadBalancerFilters } from './filter/LoadBalancerFilters';
|
|
10
10
|
import type { INestedState, StateConfigProvider } from '../navigation';
|
|
11
11
|
|
|
12
|
+
export interface ILoadBalancerStateParams {
|
|
13
|
+
name: string;
|
|
14
|
+
accountId: string;
|
|
15
|
+
region: string;
|
|
16
|
+
vpcId: string;
|
|
17
|
+
provider: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
export const LOAD_BALANCER_STATES = 'spinnaker.core.loadBalancer.states';
|
|
13
21
|
module(LOAD_BALANCER_STATES, [APPLICATION_STATE_PROVIDER]).config([
|
|
14
22
|
'applicationStateProvider',
|
|
@@ -33,12 +41,13 @@ module(LOAD_BALANCER_STATES, [APPLICATION_STATE_PROVIDER]).config([
|
|
|
33
41
|
accountId: ['$stateParams', ($stateParams: StateParams) => $stateParams.accountId],
|
|
34
42
|
loadBalancer: [
|
|
35
43
|
'$stateParams',
|
|
36
|
-
($stateParams: StateParams) => {
|
|
44
|
+
($stateParams: StateParams): ILoadBalancerStateParams => {
|
|
37
45
|
return {
|
|
38
46
|
name: $stateParams.name,
|
|
39
47
|
accountId: $stateParams.accountId,
|
|
40
48
|
region: $stateParams.region,
|
|
41
49
|
vpcId: $stateParams.vpcId,
|
|
50
|
+
provider: $stateParams.provider,
|
|
42
51
|
};
|
|
43
52
|
},
|
|
44
53
|
],
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { shallow } from 'enzyme';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Subject } from 'rxjs';
|
|
4
|
+
|
|
5
|
+
import { JobManifestPodLogs } from './JobManifestPodLogs';
|
|
6
|
+
import { JobStageExecutionLogs } from './JobStageExecutionLogs';
|
|
7
|
+
import { ManifestReader } from '../ManifestReader';
|
|
8
|
+
import type { IPodNameProvider } from '../PodNameProvider';
|
|
9
|
+
import type { Application } from '../../application';
|
|
10
|
+
|
|
11
|
+
describe('JobStageExecutionLogs', () => {
|
|
12
|
+
const mockApplication = {} as Application;
|
|
13
|
+
const mockManifest: {
|
|
14
|
+
manifest: { metadata: { name: string; namespace: string }; spec: {}; status: {} };
|
|
15
|
+
name: string;
|
|
16
|
+
moniker: { app: string; cluster: string };
|
|
17
|
+
account: string;
|
|
18
|
+
} = {
|
|
19
|
+
account: 'test-account',
|
|
20
|
+
name: 'test-manifest',
|
|
21
|
+
moniker: {
|
|
22
|
+
app: 'testapp',
|
|
23
|
+
cluster: 'testcluster',
|
|
24
|
+
},
|
|
25
|
+
manifest: {
|
|
26
|
+
metadata: {
|
|
27
|
+
name: 'test-job',
|
|
28
|
+
namespace: 'test-namespace',
|
|
29
|
+
},
|
|
30
|
+
spec: {},
|
|
31
|
+
status: {},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const mockPodNamesProviders: IPodNameProvider[] = [
|
|
35
|
+
{
|
|
36
|
+
getPodName: () => 'test-pod',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
let getManifestSpy: jasmine.Spy;
|
|
41
|
+
const subject = new Subject();
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
getManifestSpy = spyOn(ManifestReader, 'getManifest').and.returnValue(subject);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
getManifestSpy.calls.reset();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should fetch manifest on mount', () => {
|
|
52
|
+
shallow(
|
|
53
|
+
<JobStageExecutionLogs
|
|
54
|
+
deployedName="test-job"
|
|
55
|
+
account="test-account"
|
|
56
|
+
application={mockApplication}
|
|
57
|
+
externalLink=""
|
|
58
|
+
podNamesProviders={mockPodNamesProviders}
|
|
59
|
+
location="test-namespace"
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(getManifestSpy).toHaveBeenCalledWith('test-account', 'test-namespace', 'test-job');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should render JobManifestPodLogs when location is provided and no externalLink', () => {
|
|
67
|
+
const wrapper = shallow(
|
|
68
|
+
<JobStageExecutionLogs
|
|
69
|
+
deployedName="test-job"
|
|
70
|
+
account="test-account"
|
|
71
|
+
application={mockApplication}
|
|
72
|
+
externalLink=""
|
|
73
|
+
podNamesProviders={mockPodNamesProviders}
|
|
74
|
+
location="test-namespace"
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
subject.next(mockManifest);
|
|
79
|
+
wrapper.update();
|
|
80
|
+
|
|
81
|
+
const podLogs = wrapper.find(JobManifestPodLogs);
|
|
82
|
+
expect(podLogs.exists()).toBeTruthy();
|
|
83
|
+
expect(podLogs.props()).toEqual({
|
|
84
|
+
account: 'test-account',
|
|
85
|
+
location: 'test-namespace',
|
|
86
|
+
podNamesProviders: mockPodNamesProviders,
|
|
87
|
+
linkName: 'Console Output',
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should not render JobManifestPodLogs when location is not provided', () => {
|
|
92
|
+
const wrapper = shallow(
|
|
93
|
+
<JobStageExecutionLogs
|
|
94
|
+
deployedName="test-job"
|
|
95
|
+
account="test-account"
|
|
96
|
+
application={mockApplication}
|
|
97
|
+
externalLink=""
|
|
98
|
+
podNamesProviders={mockPodNamesProviders}
|
|
99
|
+
location=""
|
|
100
|
+
/>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
subject.next(mockManifest);
|
|
104
|
+
wrapper.update();
|
|
105
|
+
|
|
106
|
+
expect(wrapper.find(JobManifestPodLogs).exists()).toBeFalsy();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should render external link when provided and manifest is not empty', () => {
|
|
110
|
+
const wrapper = shallow(
|
|
111
|
+
<JobStageExecutionLogs
|
|
112
|
+
deployedName="test-job"
|
|
113
|
+
account="test-account"
|
|
114
|
+
application={mockApplication}
|
|
115
|
+
externalLink="https://example.com/logs"
|
|
116
|
+
podNamesProviders={mockPodNamesProviders}
|
|
117
|
+
location="test-namespace"
|
|
118
|
+
/>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
subject.next(mockManifest);
|
|
122
|
+
wrapper.update();
|
|
123
|
+
|
|
124
|
+
const link = wrapper.find('a');
|
|
125
|
+
expect(link.exists()).toBeTruthy();
|
|
126
|
+
expect(link.prop('href')).toBe('https://example.com/logs');
|
|
127
|
+
expect(link.text()).toBe('Console Output (External)');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should render external link with template variables', () => {
|
|
131
|
+
const wrapper = shallow(
|
|
132
|
+
<JobStageExecutionLogs
|
|
133
|
+
deployedName="test-job"
|
|
134
|
+
account="test-account"
|
|
135
|
+
application={mockApplication}
|
|
136
|
+
externalLink="https://example.com/logs/{{manifest.metadata.namespace}}/{{manifest.metadata.name}}"
|
|
137
|
+
podNamesProviders={mockPodNamesProviders}
|
|
138
|
+
location="test-namespace"
|
|
139
|
+
/>,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
subject.next(mockManifest);
|
|
143
|
+
wrapper.update();
|
|
144
|
+
|
|
145
|
+
const link = wrapper.find('a');
|
|
146
|
+
expect(link.exists()).toBeTruthy();
|
|
147
|
+
expect(link.prop('href')).toBe('https://example.com/logs/test-namespace/test-job');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not render external link with templates when manifest is empty', () => {
|
|
151
|
+
const wrapper = shallow(
|
|
152
|
+
<JobStageExecutionLogs
|
|
153
|
+
deployedName="test-job"
|
|
154
|
+
account="test-account"
|
|
155
|
+
application={mockApplication}
|
|
156
|
+
externalLink="https://example.com/logs/{{manifest.metadata.namespace}}/{{manifest.metadata.name}}"
|
|
157
|
+
podNamesProviders={mockPodNamesProviders}
|
|
158
|
+
location="test-namespace"
|
|
159
|
+
/>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Don't call subject.next() to simulate empty manifest state
|
|
163
|
+
|
|
164
|
+
const podLogs = wrapper.find(JobManifestPodLogs);
|
|
165
|
+
expect(podLogs.exists()).toBeTruthy();
|
|
166
|
+
expect(wrapper.find('a').exists()).toBeFalsy();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should not template link if it does not include template syntax', () => {
|
|
170
|
+
const externalLink = 'https://example.com/logs';
|
|
171
|
+
const wrapper = shallow(
|
|
172
|
+
<JobStageExecutionLogs
|
|
173
|
+
deployedName="test-job"
|
|
174
|
+
account="test-account"
|
|
175
|
+
application={mockApplication}
|
|
176
|
+
externalLink={externalLink}
|
|
177
|
+
podNamesProviders={mockPodNamesProviders}
|
|
178
|
+
location="test-namespace"
|
|
179
|
+
/>,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
subject.next(mockManifest);
|
|
183
|
+
wrapper.update();
|
|
184
|
+
|
|
185
|
+
const link = wrapper.find('a');
|
|
186
|
+
expect(link.prop('href')).toBe(externalLink);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle errors in manifest fetching gracefully', () => {
|
|
190
|
+
const wrapper = shallow(
|
|
191
|
+
<JobStageExecutionLogs
|
|
192
|
+
deployedName="test-job"
|
|
193
|
+
account="test-account"
|
|
194
|
+
application={mockApplication}
|
|
195
|
+
externalLink=""
|
|
196
|
+
podNamesProviders={mockPodNamesProviders}
|
|
197
|
+
location="test-namespace"
|
|
198
|
+
/>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Simulate an error
|
|
202
|
+
subject.error(new Error('Failed to fetch manifest'));
|
|
203
|
+
wrapper.update();
|
|
204
|
+
|
|
205
|
+
// Component shouldn't crash and should render the JobManifestPodLogs as fallback
|
|
206
|
+
expect(wrapper.find(JobManifestPodLogs).exists()).toBeTruthy();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { template } from 'lodash';
|
|
1
|
+
import { isEmpty, template } from 'lodash';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { from as observableFrom, Subject } from 'rxjs';
|
|
4
4
|
import { takeUntil } from 'rxjs/operators';
|
|
@@ -52,7 +52,7 @@ export class JobStageExecutionLogs extends React.Component<IJobStageExecutionLog
|
|
|
52
52
|
const { manifest } = this.state;
|
|
53
53
|
const { externalLink, podNamesProviders, location, account } = this.props;
|
|
54
54
|
// prefer links to external logging platforms
|
|
55
|
-
if (externalLink) {
|
|
55
|
+
if (externalLink && (!externalLink.includes('{{') || !isEmpty(manifest))) {
|
|
56
56
|
return (
|
|
57
57
|
<a target="_blank" href={this.renderExternalLink(externalLink, manifest)}>
|
|
58
58
|
Console Output (External)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook to manage modal visibility.
|
|
5
|
+
* Provides methods to open and close a modal.
|
|
6
|
+
*
|
|
7
|
+
* @param {UseModalProps} [props] - Configuration options for initial modal state.
|
|
8
|
+
* @param {boolean} [props.defaultOpen=false] - Initial visibility state of the modal.
|
|
9
|
+
* @returns {UseModalReturn} - The current state and control methods for the modal.
|
|
10
|
+
*/
|
|
11
|
+
export type UseModalReturn = {
|
|
12
|
+
open: boolean;
|
|
13
|
+
show: () => void;
|
|
14
|
+
close: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration options for the useModal hook.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {Object} UseModalProps
|
|
21
|
+
* @property {boolean} [defaultOpen=false] - Initial state of the modal.
|
|
22
|
+
*/
|
|
23
|
+
export type UseModalProps = {
|
|
24
|
+
defaultOpen?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hook to control modal visibility.
|
|
29
|
+
* Returns methods to open and close a modal, and the modal's current state.
|
|
30
|
+
*
|
|
31
|
+
* @param {UseModalProps} [props] - The configuration for the modal.
|
|
32
|
+
* @returns {UseModalReturn} - Modal visibility state and control methods.
|
|
33
|
+
*/
|
|
34
|
+
export const useModal = ({ defaultOpen = false }: UseModalProps = {}): UseModalReturn => {
|
|
35
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
36
|
+
|
|
37
|
+
const show = useCallback(() => setOpen(true), [open]);
|
|
38
|
+
const close = useCallback(() => setOpen(false), [open]);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
open,
|
|
42
|
+
show,
|
|
43
|
+
close,
|
|
44
|
+
};
|
|
45
|
+
};
|
package/src/task/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './PlatformHealthOverrideMessage';
|
|
2
2
|
export * from './monitor/TaskMonitor';
|
|
3
3
|
export * from './monitor/TaskMonitorWrapper';
|
|
4
|
+
export * from './monitor/useTaskMonitor';
|
|
4
5
|
export * from './modal/TaskReason';
|
|
5
6
|
export * from './modal/TaskMonitorModal';
|
|
6
7
|
export * from './task.read.service';
|