mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
chore(ui): add jsx-no-useless-fragment lint rule (#23666)
Signed-off-by: linghaoSu <linghao.su@daocloud.io>
This commit is contained in:
@@ -24,7 +24,8 @@ export default [
|
||||
...pluginReactConfig,
|
||||
rules: {
|
||||
'react/display-name': 'off',
|
||||
'react/no-string-refs': 'off'
|
||||
'react/no-string-refs': 'off',
|
||||
'react/jsx-no-useless-fragment': ['error', {allowExpressions: true}]
|
||||
}
|
||||
},
|
||||
eslintPluginPrettierRecommended,
|
||||
|
||||
@@ -185,65 +185,60 @@ export const ApplicationCreatePanel = (props: {
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DataLoader
|
||||
key='creation-deps'
|
||||
load={() =>
|
||||
Promise.all([
|
||||
services.projects.list('items.metadata.name').then(projects => projects.map(proj => proj.metadata.name).sort()),
|
||||
services.clusters.list().then(clusters => clusters.sort()),
|
||||
services.repos.list()
|
||||
]).then(([projects, clusters, reposInfo]) => ({projects, clusters, reposInfo}))
|
||||
}>
|
||||
{({projects, clusters, reposInfo}) => {
|
||||
const repos = reposInfo.map(info => info.repo).sort();
|
||||
const repoInfo = reposInfo.find(info => info.repo === app.spec.source.repoURL);
|
||||
if (repoInfo) {
|
||||
normalizeAppSource(app, repoInfo.type || 'git');
|
||||
}
|
||||
return (
|
||||
<div className='application-create-panel'>
|
||||
{(yamlMode && (
|
||||
<YamlEditor
|
||||
minHeight={800}
|
||||
initialEditMode={true}
|
||||
input={app}
|
||||
onCancel={() => setYamlMode(false)}
|
||||
onSave={async patch => {
|
||||
props.onAppChanged(jsonMergePatch.apply(app, JSON.parse(patch)));
|
||||
setYamlMode(false);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
)) || (
|
||||
<Form
|
||||
validateError={(a: models.Application) => ({
|
||||
'metadata.name': !a.metadata.name && 'Application Name is required',
|
||||
'spec.project': !a.spec.project && 'Project Name is required',
|
||||
'spec.source.repoURL': !a.spec.source.repoURL && 'Repository URL is required',
|
||||
'spec.source.targetRevision': !a.spec.source.targetRevision && a.spec.source.hasOwnProperty('chart') && 'Version is required',
|
||||
'spec.source.path': !a.spec.source.path && !a.spec.source.chart && 'Path is required',
|
||||
'spec.source.chart': !a.spec.source.path && !a.spec.source.chart && 'Chart is required',
|
||||
// Verify cluster URL when there is no cluster name field or the name value is empty
|
||||
'spec.destination.server':
|
||||
!a.spec.destination.server &&
|
||||
(!a.spec.destination.hasOwnProperty('name') || a.spec.destination.name === '') &&
|
||||
'Cluster URL is required',
|
||||
// Verify cluster name when there is no cluster URL field or the URL value is empty
|
||||
'spec.destination.name':
|
||||
!a.spec.destination.name &&
|
||||
(!a.spec.destination.hasOwnProperty('server') || a.spec.destination.server === '') &&
|
||||
'Cluster name is required'
|
||||
})}
|
||||
defaultValues={app}
|
||||
formDidUpdate={state => debouncedOnAppChanged(state.values as any)}
|
||||
onSubmit={onCreateApp}
|
||||
getApi={props.getFormApi}>
|
||||
{api => {
|
||||
const generalPanel = () => (
|
||||
<div className='white-box'>
|
||||
<p>GENERAL</p>
|
||||
{/*
|
||||
<DataLoader
|
||||
key='creation-deps'
|
||||
load={() =>
|
||||
Promise.all([
|
||||
services.projects.list('items.metadata.name').then(projects => projects.map(proj => proj.metadata.name).sort()),
|
||||
services.clusters.list().then(clusters => clusters.sort()),
|
||||
services.repos.list()
|
||||
]).then(([projects, clusters, reposInfo]) => ({projects, clusters, reposInfo}))
|
||||
}>
|
||||
{({projects, clusters, reposInfo}) => {
|
||||
const repos = reposInfo.map(info => info.repo).sort();
|
||||
const repoInfo = reposInfo.find(info => info.repo === app.spec.source.repoURL);
|
||||
if (repoInfo) {
|
||||
normalizeAppSource(app, repoInfo.type || 'git');
|
||||
}
|
||||
return (
|
||||
<div className='application-create-panel'>
|
||||
{(yamlMode && (
|
||||
<YamlEditor
|
||||
minHeight={800}
|
||||
initialEditMode={true}
|
||||
input={app}
|
||||
onCancel={() => setYamlMode(false)}
|
||||
onSave={async patch => {
|
||||
props.onAppChanged(jsonMergePatch.apply(app, JSON.parse(patch)));
|
||||
setYamlMode(false);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
)) || (
|
||||
<Form
|
||||
validateError={(a: models.Application) => ({
|
||||
'metadata.name': !a.metadata.name && 'Application Name is required',
|
||||
'spec.project': !a.spec.project && 'Project Name is required',
|
||||
'spec.source.repoURL': !a.spec.source.repoURL && 'Repository URL is required',
|
||||
'spec.source.targetRevision': !a.spec.source.targetRevision && a.spec.source.hasOwnProperty('chart') && 'Version is required',
|
||||
'spec.source.path': !a.spec.source.path && !a.spec.source.chart && 'Path is required',
|
||||
'spec.source.chart': !a.spec.source.path && !a.spec.source.chart && 'Chart is required',
|
||||
// Verify cluster URL when there is no cluster name field or the name value is empty
|
||||
'spec.destination.server':
|
||||
!a.spec.destination.server && (!a.spec.destination.hasOwnProperty('name') || a.spec.destination.name === '') && 'Cluster URL is required',
|
||||
// Verify cluster name when there is no cluster URL field or the URL value is empty
|
||||
'spec.destination.name':
|
||||
!a.spec.destination.name && (!a.spec.destination.hasOwnProperty('server') || a.spec.destination.server === '') && 'Cluster name is required'
|
||||
})}
|
||||
defaultValues={app}
|
||||
formDidUpdate={state => debouncedOnAppChanged(state.values as any)}
|
||||
onSubmit={onCreateApp}
|
||||
getApi={props.getFormApi}>
|
||||
{api => {
|
||||
const generalPanel = () => (
|
||||
<div className='white-box'>
|
||||
<p>GENERAL</p>
|
||||
{/*
|
||||
Need to specify "type='button'" because the default type 'submit'
|
||||
will activate yaml mode whenever enter is pressed while in the panel.
|
||||
This causes problems with some entry fields that require enter to be
|
||||
@@ -251,123 +246,148 @@ export const ApplicationCreatePanel = (props: {
|
||||
|
||||
See https://github.com/argoproj/argo-cd/issues/4576
|
||||
*/}
|
||||
{!yamlMode && (
|
||||
<button
|
||||
type='button'
|
||||
className='argo-button argo-button--base application-create-panel__yaml-button'
|
||||
onClick={() => setYamlMode(true)}>
|
||||
Edit as YAML
|
||||
</button>
|
||||
)}
|
||||
<div className='argo-form-row'>
|
||||
{!yamlMode && (
|
||||
<button
|
||||
type='button'
|
||||
className='argo-button argo-button--base application-create-panel__yaml-button'
|
||||
onClick={() => setYamlMode(true)}>
|
||||
Edit as YAML
|
||||
</button>
|
||||
)}
|
||||
<div className='argo-form-row'>
|
||||
<FormField formApi={api} label='Application Name' qeId='application-create-field-app-name' field='metadata.name' component={Text} />
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Project Name'
|
||||
qeId='application-create-field-project'
|
||||
field='spec.project'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: projects,
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
field='spec.syncPolicy.automated'
|
||||
qeId='application-create-field-sync-policy'
|
||||
component={AutoSyncFormField}
|
||||
/>
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<FormField formApi={api} field='metadata.finalizers' component={SetFinalizerOnApplication} />
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<label>Sync Options</label>
|
||||
<FormField formApi={api} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
|
||||
<ApplicationRetryOptions
|
||||
formApi={api}
|
||||
field='spec.syncPolicy.retry'
|
||||
retry={retry || (api.getFormState().values.spec.syncPolicy && api.getFormState().values.spec.syncPolicy.retry)}
|
||||
setRetry={setRetry}
|
||||
initValues={api.getFormState().values.spec.syncPolicy ? api.getFormState().values.spec.syncPolicy.retry : null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const repoType = api.getFormState().values.spec.source.repoURL.startsWith('oci://')
|
||||
? 'oci'
|
||||
: (api.getFormState().values.spec.source.hasOwnProperty('chart') && 'helm') || 'git';
|
||||
const sourcePanel = () => (
|
||||
<div className='white-box'>
|
||||
<p>SOURCE</p>
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Application Name'
|
||||
qeId='application-create-field-app-name'
|
||||
field='metadata.name'
|
||||
component={Text}
|
||||
/>
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Project Name'
|
||||
qeId='application-create-field-project'
|
||||
field='spec.project'
|
||||
label='Repository URL'
|
||||
qeId='application-create-field-repository-url'
|
||||
field='spec.source.repoURL'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: projects,
|
||||
items: repos,
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
field='spec.syncPolicy.automated'
|
||||
qeId='application-create-field-sync-policy'
|
||||
component={AutoSyncFormField}
|
||||
/>
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<FormField formApi={api} field='metadata.finalizers' component={SetFinalizerOnApplication} />
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<label>Sync Options</label>
|
||||
<FormField formApi={api} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
|
||||
<ApplicationRetryOptions
|
||||
formApi={api}
|
||||
field='spec.syncPolicy.retry'
|
||||
retry={retry || (api.getFormState().values.spec.syncPolicy && api.getFormState().values.spec.syncPolicy.retry)}
|
||||
setRetry={setRetry}
|
||||
initValues={api.getFormState().values.spec.syncPolicy ? api.getFormState().values.spec.syncPolicy.retry : null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const repoType = api.getFormState().values.spec.source.repoURL.startsWith('oci://')
|
||||
? 'oci'
|
||||
: (api.getFormState().values.spec.source.hasOwnProperty('chart') && 'helm') || 'git';
|
||||
const sourcePanel = () => (
|
||||
<div className='white-box'>
|
||||
<p>SOURCE</p>
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Repository URL'
|
||||
qeId='application-create-field-repository-url'
|
||||
field='spec.source.repoURL'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: repos,
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='columns small-2'>
|
||||
<div style={{paddingTop: '1.5em'}}>
|
||||
{(repoInfo && (
|
||||
<React.Fragment>
|
||||
<span>{(repoInfo.type || 'git').toUpperCase()}</span> <i className='fa fa-check' />
|
||||
</React.Fragment>
|
||||
)) || (
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{repoType.toUpperCase()} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
qeId='application-create-dropdown-source-repository'
|
||||
items={['git', 'helm', 'oci'].map((type: 'git' | 'helm' | 'oci') => ({
|
||||
title: type.toUpperCase(),
|
||||
action: () => {
|
||||
if (repoType !== type) {
|
||||
const updatedApp = api.getFormState().values as models.Application;
|
||||
if (normalizeAppSource(updatedApp, type)) {
|
||||
api.setAllValues(updatedApp);
|
||||
}
|
||||
<div className='columns small-2'>
|
||||
<div style={{paddingTop: '1.5em'}}>
|
||||
{(repoInfo && (
|
||||
<React.Fragment>
|
||||
<span>{(repoInfo.type || 'git').toUpperCase()}</span> <i className='fa fa-check' />
|
||||
</React.Fragment>
|
||||
)) || (
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{repoType.toUpperCase()} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
qeId='application-create-dropdown-source-repository'
|
||||
items={['git', 'helm', 'oci'].map((type: 'git' | 'helm' | 'oci') => ({
|
||||
title: type.toUpperCase(),
|
||||
action: () => {
|
||||
if (repoType !== type) {
|
||||
const updatedApp = api.getFormState().values as models.Application;
|
||||
if (normalizeAppSource(updatedApp, type)) {
|
||||
api.setAllValues(updatedApp);
|
||||
}
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(repoType === 'oci' && (
|
||||
</div>
|
||||
{(repoType === 'oci' && (
|
||||
<React.Fragment>
|
||||
<RevisionFormField formApi={api} helpIconTop={'2.5em'} repoURL={app.spec.source.repoURL} repoType={repoType} />
|
||||
<div className='argo-form-row'>
|
||||
<DataLoader
|
||||
input={{repoURL: app.spec.source.repoURL, revision: app.spec.source.targetRevision}}
|
||||
load={async src =>
|
||||
src.repoURL &&
|
||||
// TODO: for autocomplete we need to fetch paths that are used by other apps within the same project making use of the same OCI repo
|
||||
new Array<string>()
|
||||
}>
|
||||
{(paths: string[]) => (
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Path'
|
||||
qeId='application-create-field-path'
|
||||
field='spec.source.path'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: paths,
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DataLoader>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)) ||
|
||||
(repoType === 'git' && (
|
||||
<React.Fragment>
|
||||
<RevisionFormField formApi={api} helpIconTop={'2.5em'} repoURL={app.spec.source.repoURL} repoType={repoType} />
|
||||
<div className='argo-form-row'>
|
||||
<DataLoader
|
||||
input={{repoURL: app.spec.source.repoURL, revision: app.spec.source.targetRevision}}
|
||||
load={async src =>
|
||||
src.repoURL &&
|
||||
// TODO: for autocomplete we need to fetch paths that are used by other apps within the same project making use of the same OCI repo
|
||||
(src.repoURL &&
|
||||
services.repos
|
||||
.apps(src.repoURL, src.revision, app.metadata.name, app.spec.project)
|
||||
.then(apps => Array.from(new Set(apps.map(item => item.path))).sort())
|
||||
.catch(() => new Array<string>())) ||
|
||||
new Array<string>()
|
||||
}>
|
||||
{(paths: string[]) => (
|
||||
{(apps: string[]) => (
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Path'
|
||||
@@ -375,7 +395,7 @@ export const ApplicationCreatePanel = (props: {
|
||||
field='spec.source.path'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: paths,
|
||||
items: apps,
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
@@ -383,241 +403,209 @@ export const ApplicationCreatePanel = (props: {
|
||||
</DataLoader>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)) ||
|
||||
(repoType === 'git' && (
|
||||
<React.Fragment>
|
||||
<RevisionFormField formApi={api} helpIconTop={'2.5em'} repoURL={app.spec.source.repoURL} repoType={repoType} />
|
||||
<div className='argo-form-row'>
|
||||
<DataLoader
|
||||
input={{repoURL: app.spec.source.repoURL, revision: app.spec.source.targetRevision}}
|
||||
load={async src =>
|
||||
(src.repoURL &&
|
||||
services.repos
|
||||
.apps(src.repoURL, src.revision, app.metadata.name, app.spec.project)
|
||||
.then(apps => Array.from(new Set(apps.map(item => item.path))).sort())
|
||||
.catch(() => new Array<string>())) ||
|
||||
new Array<string>()
|
||||
}>
|
||||
{(apps: string[]) => (
|
||||
)) || (
|
||||
<DataLoader
|
||||
input={{repoURL: app.spec.source.repoURL}}
|
||||
load={async src =>
|
||||
(src.repoURL && services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())) ||
|
||||
new Array<models.HelmChart>()
|
||||
}>
|
||||
{(charts: models.HelmChart[]) => {
|
||||
const selectedChart = charts.find(chart => chart.name === api.getFormState().values.spec.source.chart);
|
||||
return (
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Path'
|
||||
qeId='application-create-field-path'
|
||||
field='spec.source.path'
|
||||
label='Chart'
|
||||
field='spec.source.chart'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: apps,
|
||||
items: charts.map(chart => chart.name),
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DataLoader>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)) || (
|
||||
<DataLoader
|
||||
input={{repoURL: app.spec.source.repoURL}}
|
||||
load={async src =>
|
||||
(src.repoURL && services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())) ||
|
||||
new Array<models.HelmChart>()
|
||||
}>
|
||||
{(charts: models.HelmChart[]) => {
|
||||
const selectedChart = charts.find(chart => chart.name === api.getFormState().values.spec.source.chart);
|
||||
return (
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Chart'
|
||||
field='spec.source.chart'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: charts.map(chart => chart.name),
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='columns small-2'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
field='spec.source.targetRevision'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: (selectedChart && selectedChart.versions) || [],
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
<RevisionHelpIcon type='helm' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className='columns small-2'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
field='spec.source.targetRevision'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: (selectedChart && selectedChart.versions) || [],
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
<RevisionHelpIcon type='helm' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const destinationPanel = () => (
|
||||
<div className='white-box'>
|
||||
<p>DESTINATION</p>
|
||||
<div className='row argo-form-row'>
|
||||
{(destinationComboValue.toUpperCase() === 'URL' && (
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Cluster URL'
|
||||
qeId='application-create-field-cluster-url'
|
||||
field='spec.destination.server'
|
||||
componentProps={{
|
||||
items: clusters.map(cluster => cluster.server),
|
||||
filterSuggestions: true
|
||||
}}
|
||||
</DataLoader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const destinationPanel = () => (
|
||||
<div className='white-box'>
|
||||
<p>DESTINATION</p>
|
||||
<div className='row argo-form-row'>
|
||||
{(destinationComboValue.toUpperCase() === 'URL' && (
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Cluster URL'
|
||||
qeId='application-create-field-cluster-url'
|
||||
field='spec.destination.server'
|
||||
componentProps={{
|
||||
items: clusters.map(cluster => cluster.server),
|
||||
filterSuggestions: true
|
||||
}}
|
||||
component={AutocompleteField}
|
||||
/>
|
||||
</div>
|
||||
)) || (
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Cluster Name'
|
||||
qeId='application-create-field-cluster-name'
|
||||
field='spec.destination.name'
|
||||
componentProps={{
|
||||
items: clusters.map(cluster => cluster.name),
|
||||
filterSuggestions: true
|
||||
}}
|
||||
component={AutocompleteField}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='columns small-2'>
|
||||
<div style={{paddingTop: '1.5em'}}>
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{destinationComboValue} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
qeId='application-create-dropdown-destination'
|
||||
items={['URL', 'NAME'].map((type: 'URL' | 'NAME') => ({
|
||||
title: type,
|
||||
action: () => {
|
||||
if (destinationComboValue !== type) {
|
||||
destinationComboValue = type;
|
||||
comboSwitchedFromPanel.current = true;
|
||||
setDestinationFieldChanges({destFormat: type, destFormatChanged: 'changed'});
|
||||
}
|
||||
component={AutocompleteField}
|
||||
/>
|
||||
</div>
|
||||
)) || (
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Cluster Name'
|
||||
qeId='application-create-field-cluster-name'
|
||||
field='spec.destination.name'
|
||||
componentProps={{
|
||||
items: clusters.map(cluster => cluster.name),
|
||||
filterSuggestions: true
|
||||
}}
|
||||
component={AutocompleteField}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='columns small-2'>
|
||||
<div style={{paddingTop: '1.5em'}}>
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{destinationComboValue} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
qeId='application-create-dropdown-destination'
|
||||
items={['URL', 'NAME'].map((type: 'URL' | 'NAME') => ({
|
||||
title: type,
|
||||
action: () => {
|
||||
if (destinationComboValue !== type) {
|
||||
destinationComboValue = type;
|
||||
comboSwitchedFromPanel.current = true;
|
||||
setDestinationFieldChanges({destFormat: type, destFormatChanged: 'changed'});
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<FormField
|
||||
qeId='application-create-field-namespace'
|
||||
formApi={api}
|
||||
label='Namespace'
|
||||
field='spec.destination.namespace'
|
||||
component={Text}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className='argo-form-row'>
|
||||
<FormField
|
||||
qeId='application-create-field-namespace'
|
||||
formApi={api}
|
||||
label='Namespace'
|
||||
field='spec.destination.namespace'
|
||||
component={Text}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const typePanel = () => (
|
||||
<DataLoader
|
||||
input={{
|
||||
repoURL: app.spec.source.repoURL,
|
||||
path: app.spec.source.path,
|
||||
chart: app.spec.source.chart,
|
||||
targetRevision: app.spec.source.targetRevision,
|
||||
appName: app.metadata.name
|
||||
}}
|
||||
load={async src => {
|
||||
if (src.repoURL && src.targetRevision && (src.path || src.chart)) {
|
||||
return services.repos.appDetails(src, src.appName, app.spec.project, 0, 0).catch(() => ({
|
||||
type: 'Directory',
|
||||
details: {}
|
||||
}));
|
||||
} else {
|
||||
return {
|
||||
type: 'Directory',
|
||||
details: {}
|
||||
};
|
||||
const typePanel = () => (
|
||||
<DataLoader
|
||||
input={{
|
||||
repoURL: app.spec.source.repoURL,
|
||||
path: app.spec.source.path,
|
||||
chart: app.spec.source.chart,
|
||||
targetRevision: app.spec.source.targetRevision,
|
||||
appName: app.metadata.name
|
||||
}}
|
||||
load={async src => {
|
||||
if (src.repoURL && src.targetRevision && (src.path || src.chart)) {
|
||||
return services.repos.appDetails(src, src.appName, app.spec.project, 0, 0).catch(() => ({
|
||||
type: 'Directory',
|
||||
details: {}
|
||||
}));
|
||||
} else {
|
||||
return {
|
||||
type: 'Directory',
|
||||
details: {}
|
||||
};
|
||||
}
|
||||
}}>
|
||||
{(details: models.RepoAppDetails) => {
|
||||
const type = (explicitPathType && explicitPathType.path === app.spec.source.path && explicitPathType.type) || details.type;
|
||||
if (details.type !== type) {
|
||||
switch (type) {
|
||||
case 'Helm':
|
||||
details = {
|
||||
type,
|
||||
path: details.path,
|
||||
helm: {name: '', valueFiles: [], path: '', parameters: [], fileParameters: []}
|
||||
};
|
||||
break;
|
||||
case 'Kustomize':
|
||||
details = {type, path: details.path, kustomize: {path: ''}};
|
||||
break;
|
||||
case 'Plugin':
|
||||
details = {type, path: details.path, plugin: {name: '', env: []}};
|
||||
break;
|
||||
// Directory
|
||||
default:
|
||||
details = {type, path: details.path, directory: {}};
|
||||
break;
|
||||
}
|
||||
}}>
|
||||
{(details: models.RepoAppDetails) => {
|
||||
const type = (explicitPathType && explicitPathType.path === app.spec.source.path && explicitPathType.type) || details.type;
|
||||
if (details.type !== type) {
|
||||
switch (type) {
|
||||
case 'Helm':
|
||||
details = {
|
||||
type,
|
||||
path: details.path,
|
||||
helm: {name: '', valueFiles: [], path: '', parameters: [], fileParameters: []}
|
||||
};
|
||||
break;
|
||||
case 'Kustomize':
|
||||
details = {type, path: details.path, kustomize: {path: ''}};
|
||||
break;
|
||||
case 'Plugin':
|
||||
details = {type, path: details.path, plugin: {name: '', env: []}};
|
||||
break;
|
||||
// Directory
|
||||
default:
|
||||
details = {type, path: details.path, directory: {}};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{type} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
qeId='application-create-dropdown-source'
|
||||
items={appTypes.map(item => ({
|
||||
title: item.type,
|
||||
action: () => {
|
||||
setExplicitPathType({type: item.type, path: app.spec.source.path});
|
||||
normalizeTypeFields(api, item.type);
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
<ApplicationParameters
|
||||
noReadonlyMode={true}
|
||||
application={app}
|
||||
details={details}
|
||||
save={async updatedApp => {
|
||||
api.setAllValues(updatedApp);
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{type} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
qeId='application-create-dropdown-source'
|
||||
items={appTypes.map(item => ({
|
||||
title: item.type,
|
||||
action: () => {
|
||||
setExplicitPathType({type: item.type, path: app.spec.source.path});
|
||||
normalizeTypeFields(api, item.type);
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
<ApplicationParameters
|
||||
noReadonlyMode={true}
|
||||
application={app}
|
||||
details={details}
|
||||
save={async updatedApp => {
|
||||
api.setAllValues(updatedApp);
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={api.submitForm} role='form' className='width-control'>
|
||||
{generalPanel()}
|
||||
return (
|
||||
<form onSubmit={api.submitForm} role='form' className='width-control'>
|
||||
{generalPanel()}
|
||||
|
||||
{sourcePanel()}
|
||||
{sourcePanel()}
|
||||
|
||||
{destinationPanel()}
|
||||
{destinationPanel()}
|
||||
|
||||
{typePanel()}
|
||||
</form>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
</React.Fragment>
|
||||
{typePanel()}
|
||||
</form>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,29 +12,27 @@ export const SetFinalizerOnApplication = ReactForm.FormField((props: {fieldApi:
|
||||
const isChecked = index < 0 ? false : true;
|
||||
return (
|
||||
<div className='small-12 large-6' style={{borderBottom: '0'}}>
|
||||
<React.Fragment>
|
||||
<Checkbox
|
||||
id='set-finalizer'
|
||||
checked={isChecked}
|
||||
onChange={(state: boolean) => {
|
||||
const value = getValue() || [];
|
||||
if (!state) {
|
||||
const i = value.findIndex((item: string) => item === finalizerVal);
|
||||
if (i >= 0) {
|
||||
const tmp = value.slice();
|
||||
tmp.splice(i, 1);
|
||||
setValue(tmp);
|
||||
}
|
||||
} else {
|
||||
<Checkbox
|
||||
id='set-finalizer'
|
||||
checked={isChecked}
|
||||
onChange={(state: boolean) => {
|
||||
const value = getValue() || [];
|
||||
if (!state) {
|
||||
const i = value.findIndex((item: string) => item === finalizerVal);
|
||||
if (i >= 0) {
|
||||
const tmp = value.slice();
|
||||
tmp.push(finalizerVal);
|
||||
tmp.splice(i, 1);
|
||||
setValue(tmp);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`set-finalizer`}>Set Deletion Finalizer</label>
|
||||
<HelpIcon title='If checked, the resources deletion finalizer will be set on the application. Potentially destructive, refer to the documentation for more information on the effects of the finalizer.' />
|
||||
</React.Fragment>
|
||||
} else {
|
||||
const tmp = value.slice();
|
||||
tmp.push(finalizerVal);
|
||||
setValue(tmp);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`set-finalizer`}>Set Deletion Finalizer</label>
|
||||
<HelpIcon title='If checked, the resources deletion finalizer will be set on the application. Potentially destructive, refer to the documentation for more information on the effects of the finalizer.' />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -61,17 +61,15 @@ export const ApplicationDeploymentHistoryDetails = ({app, info, index}: props) =
|
||||
|
||||
const getExpandedSection = (index?: number): React.ReactFragment => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div id={index ? `'show-parameters-'${index}` : 'show-parameters'} className='editable-panel__collapsible-button' style={{zIndex: 1001}}>
|
||||
<i
|
||||
className={`fa fa-angle-up filter__collapse editable-panel__collapsible-button__override`}
|
||||
onClick={() => {
|
||||
setShowParameterDetails(!showParameterDetails);
|
||||
updateMap(index);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<div id={index ? `'show-parameters-'${index}` : 'show-parameters'} className='editable-panel__collapsible-button' style={{zIndex: 1001}}>
|
||||
<i
|
||||
className={`fa fa-angle-up filter__collapse editable-panel__collapsible-button__override`}
|
||||
onClick={() => {
|
||||
setShowParameterDetails(!showParameterDetails);
|
||||
updateMap(index);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -60,12 +60,9 @@ const RenderContainerState = (props: {container: any}) => {
|
||||
{' '}
|
||||
It exited with <span className='application-node-info__container--highlight'>exit code {props.container.state.terminated.exitCode}.</span>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{' '}
|
||||
It is <span className='application-node-info__container--highlight'>{props.container?.started ? 'started' : 'not started'}</span>
|
||||
<span className='application-node-info__container--highlight'>{status === 'Completed' ? '.' : props.container?.ready ? ' and ready.' : ' and not ready.'}</span>
|
||||
</>
|
||||
)}{' '}
|
||||
It is <span className='application-node-info__container--highlight'>{props.container?.started ? 'started' : 'not started'}</span>
|
||||
<span className='application-node-info__container--highlight'>{status === 'Completed' ? '.' : props.container?.ready ? ' and ready.' : ' and not ready.'}</span>
|
||||
<br />
|
||||
{lastState && (
|
||||
<>
|
||||
|
||||
@@ -50,29 +50,27 @@ export class ApplicationParametersSource<T = {}> extends React.Component<Applica
|
||||
{ctx => (
|
||||
<div className={classNames({'editable-panel--disabled': this.state.savingTop})}>
|
||||
{this.props.floatingTitle && <div className='white-box--additional-top-space editable-panel__sticky-title'>{this.props.floatingTitle}</div>}
|
||||
<React.Fragment>
|
||||
<EditableSection
|
||||
uniqueId={'top_' + this.props.index}
|
||||
title={this.props.titleTop}
|
||||
view={this.props.viewTop}
|
||||
values={this.props.valuesTop}
|
||||
items={this.props.itemsTop}
|
||||
validate={this.props.validateTop}
|
||||
save={this.props.saveTop}
|
||||
onModeSwitch={() => this.onModeSwitch()}
|
||||
noReadonlyMode={this.props.noReadonlyMode}
|
||||
edit={this.props.editTop}
|
||||
collapsible={this.props.collapsible}
|
||||
ctx={ctx}
|
||||
isTopSection={true}
|
||||
disabledState={this.state.editTop || this.state.editTop === null}
|
||||
disabledDelete={this.props.numberOfSources <= 1}
|
||||
updateButtons={editClicked => {
|
||||
this.setState({editBottom: editClicked});
|
||||
}}
|
||||
deleteSource={this.props.deleteSource}
|
||||
/>
|
||||
</React.Fragment>
|
||||
<EditableSection
|
||||
uniqueId={'top_' + this.props.index}
|
||||
title={this.props.titleTop}
|
||||
view={this.props.viewTop}
|
||||
values={this.props.valuesTop}
|
||||
items={this.props.itemsTop}
|
||||
validate={this.props.validateTop}
|
||||
save={this.props.saveTop}
|
||||
onModeSwitch={() => this.onModeSwitch()}
|
||||
noReadonlyMode={this.props.noReadonlyMode}
|
||||
edit={this.props.editTop}
|
||||
collapsible={this.props.collapsible}
|
||||
ctx={ctx}
|
||||
isTopSection={true}
|
||||
disabledState={this.state.editTop || this.state.editTop === null}
|
||||
disabledDelete={this.props.numberOfSources <= 1}
|
||||
updateButtons={editClicked => {
|
||||
this.setState({editBottom: editClicked});
|
||||
}}
|
||||
deleteSource={this.props.deleteSource}
|
||||
/>
|
||||
{this.props.itemsTop && (
|
||||
<React.Fragment>
|
||||
<div className='row white-box__details-row'>
|
||||
@@ -81,27 +79,25 @@ export class ApplicationParametersSource<T = {}> extends React.Component<Applica
|
||||
<div className='white-box--no-padding editable-panel__divider' />
|
||||
</React.Fragment>
|
||||
)}
|
||||
<React.Fragment>
|
||||
<EditableSection
|
||||
uniqueId={'bottom_' + this.props.index}
|
||||
title={this.props.titleBottom}
|
||||
view={this.props.viewBottom}
|
||||
values={this.props.valuesBottom}
|
||||
items={this.props.itemsBottom}
|
||||
validate={this.props.validateBottom}
|
||||
save={this.props.saveBottom}
|
||||
onModeSwitch={() => this.onModeSwitch()}
|
||||
noReadonlyMode={this.props.noReadonlyMode}
|
||||
edit={this.props.editBottom}
|
||||
collapsible={this.props.collapsible}
|
||||
ctx={ctx}
|
||||
isTopSection={false}
|
||||
disabledState={this.state.editBottom || this.state.editBottom === null}
|
||||
updateButtons={editClicked => {
|
||||
this.setState({editTop: editClicked});
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
<EditableSection
|
||||
uniqueId={'bottom_' + this.props.index}
|
||||
title={this.props.titleBottom}
|
||||
view={this.props.viewBottom}
|
||||
values={this.props.valuesBottom}
|
||||
items={this.props.itemsBottom}
|
||||
validate={this.props.validateBottom}
|
||||
save={this.props.saveBottom}
|
||||
onModeSwitch={() => this.onModeSwitch()}
|
||||
noReadonlyMode={this.props.noReadonlyMode}
|
||||
edit={this.props.editBottom}
|
||||
collapsible={this.props.collapsible}
|
||||
ctx={ctx}
|
||||
isTopSection={false}
|
||||
disabledState={this.state.editBottom || this.state.editBottom === null}
|
||||
updateButtons={editClicked => {
|
||||
this.setState({editTop: editClicked});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Consumer>
|
||||
|
||||
@@ -318,16 +318,14 @@ export const ApplicationParameters = (props: {
|
||||
<div key={'app_params_expanded_' + index} className={classNames('white-box', 'editable-panel')} style={{marginBottom: '18px', paddingBottom: '20px'}}>
|
||||
<div key={'app_params_panel_' + index} className='white-box__details'>
|
||||
{collapsible && (
|
||||
<React.Fragment>
|
||||
<div className='editable-panel__collapsible-button'>
|
||||
<i
|
||||
className={`fa fa-angle-up filter__collapse editable-panel__collapsible-button__override`}
|
||||
onClick={() => {
|
||||
props.handleCollapse(index, !props.collapsedSources[index]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<div className='editable-panel__collapsible-button'>
|
||||
<i
|
||||
className={`fa fa-angle-up filter__collapse editable-panel__collapsible-button__override`}
|
||||
onClick={() => {
|
||||
props.handleCollapse(index, !props.collapsedSources[index]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<DataLoader
|
||||
key={'app_params_source_' + index}
|
||||
|
||||
@@ -86,133 +86,170 @@ export const SourcePanel = (props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DataLoader key='add-new-source' load={() => Promise.all([services.repos.list()]).then(([reposInfo]) => ({reposInfo}))}>
|
||||
{({reposInfo}) => {
|
||||
const repos = reposInfo.map(info => info.repo).sort();
|
||||
return (
|
||||
<div className='new-source-panel'>
|
||||
<Form
|
||||
validateError={(a: models.Application) => {
|
||||
let samePath = false;
|
||||
let sameChartVersion = false;
|
||||
let pathError = null;
|
||||
let chartError = null;
|
||||
if (a.spec.source.repoURL && a.spec.source.path) {
|
||||
props.appCurrent.spec.sources.forEach(source => {
|
||||
if (source.repoURL === a.spec.source.repoURL && source.path === a.spec.source.path) {
|
||||
samePath = true;
|
||||
pathError = 'Provided path in the selected repository URL was already added to this multi-source application';
|
||||
}
|
||||
});
|
||||
}
|
||||
if (a.spec?.source?.repoURL && a.spec?.source?.chart) {
|
||||
props.appCurrent.spec.sources.forEach(source => {
|
||||
if (
|
||||
source?.repoURL === a.spec?.source?.repoURL &&
|
||||
source?.chart === a.spec?.source?.chart &&
|
||||
source?.targetRevision === a.spec?.source?.targetRevision
|
||||
) {
|
||||
sameChartVersion = true;
|
||||
chartError =
|
||||
'Version ' +
|
||||
source?.targetRevision +
|
||||
' of chart ' +
|
||||
source?.chart +
|
||||
' from the selected repository was already added to this multi-source application';
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!samePath) {
|
||||
if (!a.spec?.source?.path && !a.spec?.source?.chart && !a.spec?.source?.ref) {
|
||||
pathError = 'Path or Ref is required';
|
||||
<DataLoader key='add-new-source' load={() => Promise.all([services.repos.list()]).then(([reposInfo]) => ({reposInfo}))}>
|
||||
{({reposInfo}) => {
|
||||
const repos = reposInfo.map(info => info.repo).sort();
|
||||
return (
|
||||
<div className='new-source-panel'>
|
||||
<Form
|
||||
validateError={(a: models.Application) => {
|
||||
let samePath = false;
|
||||
let sameChartVersion = false;
|
||||
let pathError = null;
|
||||
let chartError = null;
|
||||
if (a.spec.source.repoURL && a.spec.source.path) {
|
||||
props.appCurrent.spec.sources.forEach(source => {
|
||||
if (source.repoURL === a.spec.source.repoURL && source.path === a.spec.source.path) {
|
||||
samePath = true;
|
||||
pathError = 'Provided path in the selected repository URL was already added to this multi-source application';
|
||||
}
|
||||
}
|
||||
if (!sameChartVersion) {
|
||||
if (!a.spec?.source?.chart && !a.spec?.source?.path && !a.spec?.source?.ref) {
|
||||
chartError = 'Chart is required';
|
||||
});
|
||||
}
|
||||
if (a.spec?.source?.repoURL && a.spec?.source?.chart) {
|
||||
props.appCurrent.spec.sources.forEach(source => {
|
||||
if (
|
||||
source?.repoURL === a.spec?.source?.repoURL &&
|
||||
source?.chart === a.spec?.source?.chart &&
|
||||
source?.targetRevision === a.spec?.source?.targetRevision
|
||||
) {
|
||||
sameChartVersion = true;
|
||||
chartError =
|
||||
'Version ' +
|
||||
source?.targetRevision +
|
||||
' of chart ' +
|
||||
source?.chart +
|
||||
' from the selected repository was already added to this multi-source application';
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!samePath) {
|
||||
if (!a.spec?.source?.path && !a.spec?.source?.chart && !a.spec?.source?.ref) {
|
||||
pathError = 'Path or Ref is required';
|
||||
}
|
||||
return {
|
||||
'spec.source.repoURL': !a.spec?.source?.repoURL && 'Repository URL is required',
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
'spec.source.targetRevision': !a.spec?.source?.targetRevision && a.spec?.source?.hasOwnProperty('chart') && 'Version is required',
|
||||
'spec.source.path': pathError,
|
||||
'spec.source.chart': chartError
|
||||
};
|
||||
}}
|
||||
defaultValues={appInEdit}
|
||||
onSubmitFailure={(errors: FormErrors) => {
|
||||
let errorString: string = '';
|
||||
let i = 0;
|
||||
for (const key in errors) {
|
||||
if (errors[key]) {
|
||||
i++;
|
||||
errorString = errorString.concat(i + '. ' + errors[key] + ' ');
|
||||
}
|
||||
}
|
||||
if (!sameChartVersion) {
|
||||
if (!a.spec?.source?.chart && !a.spec?.source?.path && !a.spec?.source?.ref) {
|
||||
chartError = 'Chart is required';
|
||||
}
|
||||
props.onSubmitFailure(errorString);
|
||||
}}
|
||||
onSubmit={values => {
|
||||
props.updateApp(values as models.Application);
|
||||
}}
|
||||
getApi={props.getFormApi}>
|
||||
{api => {
|
||||
const repoType = api.getFormState().values.spec?.source?.repoURL.startsWith('oci://')
|
||||
? 'oci'
|
||||
: (api.getFormState().values.spec?.source?.chart && 'helm') || 'git';
|
||||
const repoInfo = reposInfo.find(info => info.repo === api.getFormState().values.spec?.source?.repoURL);
|
||||
if (repoInfo) {
|
||||
normalizeAppSource(appInEdit, repoInfo.type || 'git');
|
||||
}
|
||||
return {
|
||||
'spec.source.repoURL': !a.spec?.source?.repoURL && 'Repository URL is required',
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
'spec.source.targetRevision': !a.spec?.source?.targetRevision && a.spec?.source?.hasOwnProperty('chart') && 'Version is required',
|
||||
'spec.source.path': pathError,
|
||||
'spec.source.chart': chartError
|
||||
};
|
||||
}}
|
||||
defaultValues={appInEdit}
|
||||
onSubmitFailure={(errors: FormErrors) => {
|
||||
let errorString: string = '';
|
||||
let i = 0;
|
||||
for (const key in errors) {
|
||||
if (errors[key]) {
|
||||
i++;
|
||||
errorString = errorString.concat(i + '. ' + errors[key] + ' ');
|
||||
}
|
||||
const sourcePanel = () => (
|
||||
<div className='white-box'>
|
||||
<p>SOURCE</p>
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Repository URL'
|
||||
field='spec.source.repoURL'
|
||||
component={AutocompleteField}
|
||||
componentProps={{items: repos}}
|
||||
/>
|
||||
</div>
|
||||
<div className='columns small-2'>
|
||||
<div style={{paddingTop: '1.5em'}}>
|
||||
{(repoInfo && (
|
||||
<React.Fragment>
|
||||
<span>{(repoInfo.type || 'git').toUpperCase()}</span> <i className='fa fa-check' />
|
||||
</React.Fragment>
|
||||
)) || (
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{repoType.toUpperCase()} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
items={['git', 'helm', 'oci'].map((type: 'git' | 'helm' | 'oci') => ({
|
||||
title: type.toUpperCase(),
|
||||
action: () => {
|
||||
if (repoType !== type) {
|
||||
const updatedApp = api.getFormState().values as models.Application;
|
||||
if (normalizeAppSource(updatedApp, type)) {
|
||||
api.setAllValues(updatedApp);
|
||||
}
|
||||
}
|
||||
props.onSubmitFailure(errorString);
|
||||
}}
|
||||
onSubmit={values => {
|
||||
props.updateApp(values as models.Application);
|
||||
}}
|
||||
getApi={props.getFormApi}>
|
||||
{api => {
|
||||
const repoType = api.getFormState().values.spec?.source?.repoURL.startsWith('oci://')
|
||||
? 'oci'
|
||||
: (api.getFormState().values.spec?.source?.chart && 'helm') || 'git';
|
||||
const repoInfo = reposInfo.find(info => info.repo === api.getFormState().values.spec?.source?.repoURL);
|
||||
if (repoInfo) {
|
||||
normalizeAppSource(appInEdit, repoInfo.type || 'git');
|
||||
}
|
||||
const sourcePanel = () => (
|
||||
<div className='white-box'>
|
||||
<p>SOURCE</p>
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Repository URL'
|
||||
field='spec.source.repoURL'
|
||||
component={AutocompleteField}
|
||||
componentProps={{items: repos}}
|
||||
/>
|
||||
</div>
|
||||
<div className='columns small-2'>
|
||||
<div style={{paddingTop: '1.5em'}}>
|
||||
{(repoInfo && (
|
||||
<React.Fragment>
|
||||
<span>{(repoInfo.type || 'git').toUpperCase()}</span> <i className='fa fa-check' />
|
||||
</React.Fragment>
|
||||
)) || (
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{repoType.toUpperCase()} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
items={['git', 'helm', 'oci'].map((type: 'git' | 'helm' | 'oci') => ({
|
||||
title: type.toUpperCase(),
|
||||
action: () => {
|
||||
if (repoType !== type) {
|
||||
const updatedApp = api.getFormState().values as models.Application;
|
||||
if (normalizeAppSource(updatedApp, type)) {
|
||||
api.setAllValues(updatedApp);
|
||||
}
|
||||
}
|
||||
}))}
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField formApi={api} label='Name' field={'spec.source.name'} component={Text}></FormField>
|
||||
</div>
|
||||
</div>
|
||||
{(repoType === 'oci' && (
|
||||
<React.Fragment>
|
||||
<RevisionFormField
|
||||
formApi={api}
|
||||
helpIconTop={'2.5em'}
|
||||
repoURL={api.getFormState().values.spec?.source?.repoURL}
|
||||
repoType={repoType}
|
||||
/>
|
||||
<div className='argo-form-row'>
|
||||
<DataLoader
|
||||
input={{
|
||||
repoURL: api.getFormState().values.spec?.source?.repoURL,
|
||||
revision: api.getFormState().values.spec?.source?.targetRevision
|
||||
}}
|
||||
load={async src =>
|
||||
src.repoURL &&
|
||||
// TODO: for autocomplete we need to fetch paths that are used by other apps within the same project making use of the same OCI repo
|
||||
new Array<string>()
|
||||
}>
|
||||
{(paths: string[]) => (
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Path'
|
||||
field='spec.source.path'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: paths,
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DataLoader>
|
||||
</div>
|
||||
</div>
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField formApi={api} label='Name' field={'spec.source.name'} component={Text}></FormField>
|
||||
<div className='argo-form-row'>
|
||||
<FormField formApi={api} label='Ref' field={'spec.source.ref'} component={Text}></FormField>
|
||||
</div>
|
||||
</div>
|
||||
{(repoType === 'oci' && (
|
||||
</React.Fragment>
|
||||
)) ||
|
||||
(repoType === 'git' && (
|
||||
<React.Fragment>
|
||||
<RevisionFormField
|
||||
formApi={api}
|
||||
@@ -227,18 +264,21 @@ export const SourcePanel = (props: {
|
||||
revision: api.getFormState().values.spec?.source?.targetRevision
|
||||
}}
|
||||
load={async src =>
|
||||
src.repoURL &&
|
||||
// TODO: for autocomplete we need to fetch paths that are used by other apps within the same project making use of the same OCI repo
|
||||
(src.repoURL &&
|
||||
(await services.repos
|
||||
.apps(src.repoURL, src.revision, appInEdit.metadata.name, props.appCurrent.spec.project)
|
||||
.then(apps => Array.from(new Set(apps.map(item => item.path))).sort())
|
||||
.catch(() => new Array<string>()))) ||
|
||||
new Array<string>()
|
||||
}>
|
||||
{(paths: string[]) => (
|
||||
{(apps: string[]) => (
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Path'
|
||||
field='spec.source.path'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: paths,
|
||||
items: apps,
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
@@ -249,178 +289,136 @@ export const SourcePanel = (props: {
|
||||
<FormField formApi={api} label='Ref' field={'spec.source.ref'} component={Text}></FormField>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)) ||
|
||||
(repoType === 'git' && (
|
||||
<React.Fragment>
|
||||
<RevisionFormField
|
||||
formApi={api}
|
||||
helpIconTop={'2.5em'}
|
||||
repoURL={api.getFormState().values.spec?.source?.repoURL}
|
||||
repoType={repoType}
|
||||
/>
|
||||
<div className='argo-form-row'>
|
||||
<DataLoader
|
||||
input={{
|
||||
repoURL: api.getFormState().values.spec?.source?.repoURL,
|
||||
revision: api.getFormState().values.spec?.source?.targetRevision
|
||||
}}
|
||||
load={async src =>
|
||||
(src.repoURL &&
|
||||
(await services.repos
|
||||
.apps(src.repoURL, src.revision, appInEdit.metadata.name, props.appCurrent.spec.project)
|
||||
.then(apps => Array.from(new Set(apps.map(item => item.path))).sort())
|
||||
.catch(() => new Array<string>()))) ||
|
||||
new Array<string>()
|
||||
}>
|
||||
{(apps: string[]) => (
|
||||
)) || (
|
||||
<DataLoader
|
||||
input={{repoURL: api.getFormState().values.spec.source.repoURL}}
|
||||
load={async src =>
|
||||
(src.repoURL && services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())) ||
|
||||
new Array<models.HelmChart>()
|
||||
}>
|
||||
{(charts: models.HelmChart[]) => {
|
||||
const selectedChart = charts.find(chart => chart.name === api.getFormState().values.spec?.source?.chart);
|
||||
return (
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Path'
|
||||
field='spec.source.path'
|
||||
label='Chart'
|
||||
field='spec.source.chart'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: apps,
|
||||
items: charts.map(chart => chart.name),
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DataLoader>
|
||||
</div>
|
||||
<div className='argo-form-row'>
|
||||
<FormField formApi={api} label='Ref' field={'spec.source.ref'} component={Text}></FormField>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)) || (
|
||||
<DataLoader
|
||||
input={{repoURL: api.getFormState().values.spec.source.repoURL}}
|
||||
load={async src =>
|
||||
(src.repoURL && services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())) ||
|
||||
new Array<models.HelmChart>()
|
||||
}>
|
||||
{(charts: models.HelmChart[]) => {
|
||||
const selectedChart = charts.find(chart => chart.name === api.getFormState().values.spec?.source?.chart);
|
||||
return (
|
||||
<div className='row argo-form-row'>
|
||||
<div className='columns small-10'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
label='Chart'
|
||||
field='spec.source.chart'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: charts.map(chart => chart.name),
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='columns small-2'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
field='spec.source.targetRevision'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: (selectedChart && selectedChart.versions) || []
|
||||
}}
|
||||
/>
|
||||
<RevisionHelpIcon type='helm' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className='columns small-2'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
field='spec.source.targetRevision'
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: (selectedChart && selectedChart.versions) || []
|
||||
}}
|
||||
/>
|
||||
<RevisionHelpIcon type='helm' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const typePanel = () => (
|
||||
<DataLoader
|
||||
input={{
|
||||
repoURL: appInEdit.spec?.source?.repoURL,
|
||||
path: appInEdit.spec?.source?.path,
|
||||
chart: appInEdit.spec?.source?.chart,
|
||||
targetRevision: appInEdit.spec?.source?.targetRevision,
|
||||
appName: appInEdit.metadata.name
|
||||
}}
|
||||
load={async src => {
|
||||
if (src?.repoURL && src?.targetRevision && (src?.path || src?.chart)) {
|
||||
return services.repos.appDetails(src, src?.appName, props.appCurrent.spec?.project, 0, 0).catch(() => ({
|
||||
type: 'Directory',
|
||||
details: {}
|
||||
}));
|
||||
} else {
|
||||
return {
|
||||
type: 'Directory',
|
||||
details: {}
|
||||
};
|
||||
}
|
||||
}}>
|
||||
{(details: models.RepoAppDetails) => {
|
||||
const type = (explicitPathType && explicitPathType.path === appInEdit.spec?.source?.path && explicitPathType.type) || details.type;
|
||||
if (details.type !== type) {
|
||||
switch (type) {
|
||||
case 'Helm':
|
||||
details = {
|
||||
type,
|
||||
path: details.path,
|
||||
helm: {name: '', valueFiles: [], path: '', parameters: [], fileParameters: []}
|
||||
};
|
||||
break;
|
||||
case 'Kustomize':
|
||||
details = {type, path: details.path, kustomize: {path: ''}};
|
||||
break;
|
||||
case 'Plugin':
|
||||
details = {type, path: details.path, plugin: {name: '', env: []}};
|
||||
break;
|
||||
// Directory
|
||||
default:
|
||||
details = {type, path: details.path, directory: {}};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{type} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
items={appTypes.map(item => ({
|
||||
title: item.type,
|
||||
action: () => {
|
||||
setExplicitPathType({type: item.type, path: appInEdit.spec?.source?.path});
|
||||
normalizeTypeFields(api, item.type);
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
<ApplicationParameters
|
||||
noReadonlyMode={true}
|
||||
application={api.getFormState().values as models.Application}
|
||||
details={details}
|
||||
tempSource={appInEdit.spec.source}
|
||||
save={async updatedApp => {
|
||||
api.setAllValues(updatedApp);
|
||||
}}
|
||||
</DataLoader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
);
|
||||
|
||||
const typePanel = () => (
|
||||
<DataLoader
|
||||
input={{
|
||||
repoURL: appInEdit.spec?.source?.repoURL,
|
||||
path: appInEdit.spec?.source?.path,
|
||||
chart: appInEdit.spec?.source?.chart,
|
||||
targetRevision: appInEdit.spec?.source?.targetRevision,
|
||||
appName: appInEdit.metadata.name
|
||||
}}
|
||||
load={async src => {
|
||||
if (src?.repoURL && src?.targetRevision && (src?.path || src?.chart)) {
|
||||
return services.repos.appDetails(src, src?.appName, props.appCurrent.spec?.project, 0, 0).catch(() => ({
|
||||
type: 'Directory',
|
||||
details: {}
|
||||
}));
|
||||
} else {
|
||||
return {
|
||||
type: 'Directory',
|
||||
details: {}
|
||||
};
|
||||
}
|
||||
}}>
|
||||
{(details: models.RepoAppDetails) => {
|
||||
const type = (explicitPathType && explicitPathType.path === appInEdit.spec?.source?.path && explicitPathType.type) || details.type;
|
||||
if (details.type !== type) {
|
||||
switch (type) {
|
||||
case 'Helm':
|
||||
details = {
|
||||
type,
|
||||
path: details.path,
|
||||
helm: {name: '', valueFiles: [], path: '', parameters: [], fileParameters: []}
|
||||
};
|
||||
break;
|
||||
case 'Kustomize':
|
||||
details = {type, path: details.path, kustomize: {path: ''}};
|
||||
break;
|
||||
case 'Plugin':
|
||||
details = {type, path: details.path, plugin: {name: '', env: []}};
|
||||
break;
|
||||
// Directory
|
||||
default:
|
||||
details = {type, path: details.path, directory: {}};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<p>
|
||||
{type} <i className='fa fa-caret-down' />
|
||||
</p>
|
||||
)}
|
||||
items={appTypes.map(item => ({
|
||||
title: item.type,
|
||||
action: () => {
|
||||
setExplicitPathType({type: item.type, path: appInEdit.spec?.source?.path});
|
||||
normalizeTypeFields(api, item.type);
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
<ApplicationParameters
|
||||
noReadonlyMode={true}
|
||||
application={api.getFormState().values as models.Application}
|
||||
details={details}
|
||||
tempSource={appInEdit.spec.source}
|
||||
save={async updatedApp => {
|
||||
api.setAllValues(updatedApp);
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
);
|
||||
return (
|
||||
<form onSubmit={api.submitForm} role='form' className='width-control'>
|
||||
{sourcePanel()}
|
||||
|
||||
return (
|
||||
<form onSubmit={api.submitForm} role='form' className='width-control'>
|
||||
{sourcePanel()}
|
||||
|
||||
{typePanel()}
|
||||
</form>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
</React.Fragment>
|
||||
{typePanel()}
|
||||
</form>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ export class PodView extends React.Component<PodViewProps> {
|
||||
<div className='pod-view__nodes-container'>
|
||||
{groups.map(group => {
|
||||
if (group.type === 'node' && group.name === 'Unschedulable' && podPrefs.hideUnschedulable) {
|
||||
return <React.Fragment />;
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={`pod-view__node white-box ${group.kind === 'node' && 'pod-view__node--large'}`} key={group.fullName || group.name}>
|
||||
|
||||
@@ -201,87 +201,83 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh
|
||||
</div>
|
||||
)}
|
||||
<div className='application-status-panel__item'>
|
||||
<React.Fragment>
|
||||
{sectionHeader(
|
||||
{
|
||||
title: 'SYNC STATUS',
|
||||
helpContent: 'Whether or not the version of your app is up to date with your repo. You may wish to sync your app if it is out-of-sync.'
|
||||
},
|
||||
() => showMetadataInfo(application.status.sync ? 'SYNC_STATUS_REVISION' : null)
|
||||
)}
|
||||
<div className={`application-status-panel__item-value${appOperationState?.phase ? ` application-status-panel__item-value--${appOperationState.phase}` : ''}`}>
|
||||
<div>
|
||||
{application.status.sync.status === models.SyncStatuses.OutOfSync ? (
|
||||
<a onClick={() => showDiff && showDiff()}>
|
||||
<ComparisonStatusIcon status={application.status.sync.status} label={true} isButton={true} />
|
||||
</a>
|
||||
) : (
|
||||
<ComparisonStatusIcon status={application.status.sync.status} label={true} />
|
||||
)}
|
||||
</div>
|
||||
<div className='application-status-panel__item-value__revision show-for-large'>{syncStatusMessage(application)}</div>
|
||||
</div>
|
||||
<div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
|
||||
{application.spec.syncPolicy?.automated ? 'Auto sync is enabled.' : 'Auto sync is not enabled.'}
|
||||
</div>
|
||||
{application.status &&
|
||||
application.status.sync &&
|
||||
(hasMultipleSources
|
||||
? application.status.sync.revisions && application.status.sync.revisions[0] && application.spec.sources && !application.spec.sources[0].chart
|
||||
: application.status.sync.revision && !application.spec?.source?.chart) && (
|
||||
<div className='application-status-panel__item-name'>
|
||||
<RevisionMetadataPanel
|
||||
appName={application.metadata.name}
|
||||
appNamespace={application.metadata.namespace}
|
||||
type={revisionType}
|
||||
revision={revision}
|
||||
versionId={utils.getAppCurrentVersion(application)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</div>
|
||||
{appOperationState && (
|
||||
<div className='application-status-panel__item'>
|
||||
<React.Fragment>
|
||||
{sectionHeader(
|
||||
{
|
||||
title: 'LAST SYNC',
|
||||
helpContent:
|
||||
'Whether or not your last app sync was successful. It has been ' +
|
||||
daysSinceLastSynchronized +
|
||||
' days since last sync. Click for the status of that sync.'
|
||||
},
|
||||
() =>
|
||||
showMetadataInfo(
|
||||
appOperationState.syncResult && (appOperationState.syncResult.revisions || appOperationState.syncResult.revision)
|
||||
? 'OPERATION_STATE_REVISION'
|
||||
: null
|
||||
)
|
||||
)}
|
||||
<div className={`application-status-panel__item-value application-status-panel__item-value--${appOperationState.phase}`}>
|
||||
<a onClick={() => showOperation && showOperation()}>
|
||||
<OperationState app={application} isButton={true} />{' '}
|
||||
{sectionHeader(
|
||||
{
|
||||
title: 'SYNC STATUS',
|
||||
helpContent: 'Whether or not the version of your app is up to date with your repo. You may wish to sync your app if it is out-of-sync.'
|
||||
},
|
||||
() => showMetadataInfo(application.status.sync ? 'SYNC_STATUS_REVISION' : null)
|
||||
)}
|
||||
<div className={`application-status-panel__item-value${appOperationState?.phase ? ` application-status-panel__item-value--${appOperationState.phase}` : ''}`}>
|
||||
<div>
|
||||
{application.status.sync.status === models.SyncStatuses.OutOfSync ? (
|
||||
<a onClick={() => showDiff && showDiff()}>
|
||||
<ComparisonStatusIcon status={application.status.sync.status} label={true} isButton={true} />
|
||||
</a>
|
||||
{appOperationState.syncResult && (appOperationState.syncResult.revision || appOperationState.syncResult.revisions) && (
|
||||
<div className='application-status-panel__item-value__revision show-for-large'>
|
||||
to <Revision repoUrl={source.repoURL} revision={operationStateRevision} /> {getAppDefaultSyncRevisionExtra(application)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
|
||||
{appOperationState.phase} <Timestamp date={appOperationState.finishedAt || appOperationState.startedAt} />
|
||||
</div>
|
||||
{(appOperationState.syncResult && operationStateRevision && (
|
||||
) : (
|
||||
<ComparisonStatusIcon status={application.status.sync.status} label={true} />
|
||||
)}
|
||||
</div>
|
||||
<div className='application-status-panel__item-value__revision show-for-large'>{syncStatusMessage(application)}</div>
|
||||
</div>
|
||||
<div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
|
||||
{application.spec.syncPolicy?.automated ? 'Auto sync is enabled.' : 'Auto sync is not enabled.'}
|
||||
</div>
|
||||
{application.status &&
|
||||
application.status.sync &&
|
||||
(hasMultipleSources
|
||||
? application.status.sync.revisions && application.status.sync.revisions[0] && application.spec.sources && !application.spec.sources[0].chart
|
||||
: application.status.sync.revision && !application.spec?.source?.chart) && (
|
||||
<div className='application-status-panel__item-name'>
|
||||
<RevisionMetadataPanel
|
||||
appName={application.metadata.name}
|
||||
appNamespace={application.metadata.namespace}
|
||||
type={revisionType}
|
||||
revision={operationStateRevision}
|
||||
revision={revision}
|
||||
versionId={utils.getAppCurrentVersion(application)}
|
||||
/>
|
||||
)) || <div className='application-status-panel__item-name'>{appOperationState.message}</div>}
|
||||
</React.Fragment>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{appOperationState && (
|
||||
<div className='application-status-panel__item'>
|
||||
{sectionHeader(
|
||||
{
|
||||
title: 'LAST SYNC',
|
||||
helpContent:
|
||||
'Whether or not your last app sync was successful. It has been ' +
|
||||
daysSinceLastSynchronized +
|
||||
' days since last sync. Click for the status of that sync.'
|
||||
},
|
||||
() =>
|
||||
showMetadataInfo(
|
||||
appOperationState.syncResult && (appOperationState.syncResult.revisions || appOperationState.syncResult.revision)
|
||||
? 'OPERATION_STATE_REVISION'
|
||||
: null
|
||||
)
|
||||
)}
|
||||
<div className={`application-status-panel__item-value application-status-panel__item-value--${appOperationState.phase}`}>
|
||||
<a onClick={() => showOperation && showOperation()}>
|
||||
<OperationState app={application} isButton={true} />{' '}
|
||||
</a>
|
||||
{appOperationState.syncResult && (appOperationState.syncResult.revision || appOperationState.syncResult.revisions) && (
|
||||
<div className='application-status-panel__item-value__revision show-for-large'>
|
||||
to <Revision repoUrl={source.repoURL} revision={operationStateRevision} /> {getAppDefaultSyncRevisionExtra(application)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
|
||||
{appOperationState.phase} <Timestamp date={appOperationState.finishedAt || appOperationState.startedAt} />
|
||||
</div>
|
||||
{(appOperationState.syncResult && operationStateRevision && (
|
||||
<RevisionMetadataPanel
|
||||
appName={application.metadata.name}
|
||||
appNamespace={application.metadata.namespace}
|
||||
type={revisionType}
|
||||
revision={operationStateRevision}
|
||||
versionId={utils.getAppCurrentVersion(application)}
|
||||
/>
|
||||
)) || <div className='application-status-panel__item-name'>{appOperationState.message}</div>}
|
||||
</div>
|
||||
)}
|
||||
{application.status.conditions && (
|
||||
|
||||
@@ -5,7 +5,7 @@ import {services} from '../../../shared/services';
|
||||
|
||||
export const RevisionMetadataPanel = (props: {appName: string; appNamespace: string; type: string; revision: string; versionId: number}) => {
|
||||
if (props.type === 'helm' || props.type === 'oci') {
|
||||
return <React.Fragment />;
|
||||
return null;
|
||||
}
|
||||
if (props.type === 'oci') {
|
||||
return (
|
||||
|
||||
@@ -342,19 +342,17 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
|
||||
attributes.push({
|
||||
title: 'URLs',
|
||||
view: (
|
||||
<React.Fragment>
|
||||
<div className='application-summary__links-rows'>
|
||||
{urls
|
||||
.map(item => item.split('|'))
|
||||
.map((parts, i) => (
|
||||
<div className='application-summary__links-row'>
|
||||
<a key={i} href={parts.length > 1 ? parts[1] : parts[0]} target='_blank'>
|
||||
{parts[0]}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<div className='application-summary__links-rows'>
|
||||
{urls
|
||||
.map(item => item.split('|'))
|
||||
.map((parts, i) => (
|
||||
<div className='application-summary__links-row'>
|
||||
<a key={i} href={parts.length > 1 ? parts[1] : parts[0]} target='_blank'>
|
||||
{parts[0]}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -493,7 +491,7 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
|
||||
<div className='application-summary'>
|
||||
<EditablePanel
|
||||
save={updateApp}
|
||||
view={hasMultipleSources ? <>This is a multi-source app, see the Sources tab for repository URLs and source-related information.</> : <></>}
|
||||
view={hasMultipleSources ? <>This is a multi-source app, see the Sources tab for repository URLs and source-related information.</> : null}
|
||||
validate={input => ({
|
||||
'spec.project': !input.spec.project && 'Project name is required',
|
||||
'spec.destination.server': !input.spec.destination.server && input.spec.destination.hasOwnProperty('server') && 'Cluster server is required',
|
||||
|
||||
@@ -74,30 +74,28 @@ export const ApplicationsRefreshPanel = ({show, apps, hide}: {show: boolean; app
|
||||
}}
|
||||
getApi={setForm}>
|
||||
{formApi => (
|
||||
<React.Fragment>
|
||||
<div className='argo-form-row' style={{marginTop: 0}}>
|
||||
<h4>Refresh app(s)</h4>
|
||||
{progress !== null && <ProgressPopup onClose={() => setProgress(null)} percentage={progress.percentage} title={progress.title} />}
|
||||
<div style={{marginBottom: '1em'}}>
|
||||
<label>Refresh Type</label>
|
||||
<div className='row application-sync-options'>
|
||||
{RefreshTypes.map(refreshType => (
|
||||
<label key={refreshType} style={{paddingRight: '1.5em', marginTop: '0.4em'}}>
|
||||
<input
|
||||
type='radio'
|
||||
value={refreshType}
|
||||
checked={formApi.values.refreshType === refreshType}
|
||||
onChange={() => formApi.setValue('refreshType', refreshType)}
|
||||
style={{marginRight: '5px', transform: 'translateY(2px)'}}
|
||||
/>
|
||||
{refreshType}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className='argo-form-row' style={{marginTop: 0}}>
|
||||
<h4>Refresh app(s)</h4>
|
||||
{progress !== null && <ProgressPopup onClose={() => setProgress(null)} percentage={progress.percentage} title={progress.title} />}
|
||||
<div style={{marginBottom: '1em'}}>
|
||||
<label>Refresh Type</label>
|
||||
<div className='row application-sync-options'>
|
||||
{RefreshTypes.map(refreshType => (
|
||||
<label key={refreshType} style={{paddingRight: '1.5em', marginTop: '0.4em'}}>
|
||||
<input
|
||||
type='radio'
|
||||
value={refreshType}
|
||||
checked={formApi.values.refreshType === refreshType}
|
||||
onChange={() => formApi.setValue('refreshType', refreshType)}
|
||||
style={{marginRight: '5px', transform: 'translateY(2px)'}}
|
||||
/>
|
||||
{refreshType}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<ApplicationSelector apps={apps} formApi={formApi} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<ApplicationSelector apps={apps} formApi={formApi} />
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</SlidingPanel>
|
||||
|
||||
@@ -126,30 +126,28 @@ export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps:
|
||||
}}
|
||||
getApi={setForm}>
|
||||
{formApi => (
|
||||
<React.Fragment>
|
||||
<div className='argo-form-row' style={{marginTop: 0}}>
|
||||
<h4>Sync app(s)</h4>
|
||||
{progress !== null && <ProgressPopup onClose={() => setProgress(null)} percentage={progress.percentage} title={progress.title} />}
|
||||
<div style={{marginBottom: '1em'}}>
|
||||
<FormField formApi={formApi} field='syncFlags' component={ApplicationManualSyncFlags} />
|
||||
</div>
|
||||
<div style={{marginBottom: '1em'}}>
|
||||
<label>Sync Options</label>
|
||||
<ApplicationSyncOptions
|
||||
options={formApi.values.syncOptions}
|
||||
onChanged={opts => {
|
||||
formApi.setTouched('syncOptions', true);
|
||||
formApi.setValue('syncOptions', opts);
|
||||
}}
|
||||
id='applications-sync-panel'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ApplicationRetryOptions id='applications-sync-panel' formApi={formApi} />
|
||||
|
||||
<ApplicationSelector apps={apps} formApi={formApi} />
|
||||
<div className='argo-form-row' style={{marginTop: 0}}>
|
||||
<h4>Sync app(s)</h4>
|
||||
{progress !== null && <ProgressPopup onClose={() => setProgress(null)} percentage={progress.percentage} title={progress.title} />}
|
||||
<div style={{marginBottom: '1em'}}>
|
||||
<FormField formApi={formApi} field='syncFlags' component={ApplicationManualSyncFlags} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<div style={{marginBottom: '1em'}}>
|
||||
<label>Sync Options</label>
|
||||
<ApplicationSyncOptions
|
||||
options={formApi.values.syncOptions}
|
||||
onChanged={opts => {
|
||||
formApi.setTouched('syncOptions', true);
|
||||
formApi.setValue('syncOptions', opts);
|
||||
}}
|
||||
id='applications-sync-panel'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ApplicationRetryOptions id='applications-sync-panel' formApi={formApi} />
|
||||
|
||||
<ApplicationSelector apps={apps} formApi={formApi} />
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</SlidingPanel>
|
||||
|
||||
@@ -71,7 +71,7 @@ export const FiltersGroup = (props: {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<>{props.children}</>
|
||||
{props.children}
|
||||
<div className='filters-group__content'>{props.content}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ export const ContainerSelector = ({
|
||||
onClickContainer: (group: ContainerGroup, index: number, logs: string) => void;
|
||||
}) => {
|
||||
if (!containerGroups) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
const containers = containerGroups?.reduce((acc, group) => acc.concat(group.containers), []);
|
||||
@@ -25,7 +25,7 @@ export const ContainerSelector = ({
|
||||
const containerIndex = (n: string) => {
|
||||
return containerGroup(n)?.containers.findIndex(container => container.name === n);
|
||||
};
|
||||
if (containerNames.length <= 1) return <></>;
|
||||
if (containerNames.length <= 1) return null;
|
||||
return (
|
||||
<Tooltip content='Select a container to view logs' interactive={false}>
|
||||
<select className='argo-field' value={containerName} onChange={e => onClickContainer(containerGroup(e.target.value), containerIndex(e.target.value), 'logs')}>
|
||||
|
||||
@@ -296,11 +296,9 @@ export const ResourceDetails = (props: ResourceDetailsProps) => {
|
||||
</div>
|
||||
<h1>{selectedNode.name}</h1>
|
||||
{data.controlledState && (
|
||||
<React.Fragment>
|
||||
<span style={{marginRight: '5px'}}>
|
||||
<AppUtils.ComparisonStatusIcon status={data.controlledState.summary.status} resource={data.controlledState.summary} />
|
||||
</span>
|
||||
</React.Fragment>
|
||||
<span style={{marginRight: '5px'}}>
|
||||
<AppUtils.ComparisonStatusIcon status={data.controlledState.summary.status} resource={data.controlledState.summary} />
|
||||
</span>
|
||||
)}
|
||||
{(selectedNode as ResourceTreeNode).health && <AppUtils.HealthStatusIcon state={(selectedNode as ResourceTreeNode).health} />}
|
||||
<button
|
||||
|
||||
@@ -31,42 +31,40 @@ export class RevisionFormField extends React.PureComponent<RevisionFormFieldProp
|
||||
return (
|
||||
<div className={'row' + rowClass}>
|
||||
<div className='columns small-10'>
|
||||
<React.Fragment>
|
||||
<DataLoader
|
||||
input={{repoURL: this.props.repoURL, filterType: selectedFilter}}
|
||||
load={async (src: any): Promise<string[]> => {
|
||||
if (this.props.repoType === 'oci' && src.repoURL) {
|
||||
return services.repos
|
||||
.ociTags(src.repoURL)
|
||||
.then(revisionsRes => ['HEAD'].concat(revisionsRes.tags || []))
|
||||
.catch(() => []);
|
||||
} else if (src.repoURL) {
|
||||
return services.repos
|
||||
.revisions(src.repoURL)
|
||||
.then(revisionsRes =>
|
||||
['HEAD']
|
||||
.concat(selectedFilter === 'Branches' ? revisionsRes.branches || [] : [])
|
||||
.concat(selectedFilter === 'Tags' ? revisionsRes.tags || [] : [])
|
||||
)
|
||||
.catch(() => []);
|
||||
}
|
||||
return [];
|
||||
}}>
|
||||
{(revisions: string[]) => (
|
||||
<FormField
|
||||
formApi={this.props.formApi}
|
||||
label={this.props.hideLabel ? undefined : 'Revision'}
|
||||
field={this.props.fieldValue ? this.props.fieldValue : 'spec.source.targetRevision'}
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: revisions,
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DataLoader>
|
||||
<RevisionHelpIcon type='git' top={this.props.helpIconTop} right='0em' />
|
||||
</React.Fragment>
|
||||
<DataLoader
|
||||
input={{repoURL: this.props.repoURL, filterType: selectedFilter}}
|
||||
load={async (src: any): Promise<string[]> => {
|
||||
if (this.props.repoType === 'oci' && src.repoURL) {
|
||||
return services.repos
|
||||
.ociTags(src.repoURL)
|
||||
.then(revisionsRes => ['HEAD'].concat(revisionsRes.tags || []))
|
||||
.catch(() => []);
|
||||
} else if (src.repoURL) {
|
||||
return services.repos
|
||||
.revisions(src.repoURL)
|
||||
.then(revisionsRes =>
|
||||
['HEAD']
|
||||
.concat(selectedFilter === 'Branches' ? revisionsRes.branches || [] : [])
|
||||
.concat(selectedFilter === 'Tags' ? revisionsRes.tags || [] : [])
|
||||
)
|
||||
.catch(() => []);
|
||||
}
|
||||
return [];
|
||||
}}>
|
||||
{(revisions: string[]) => (
|
||||
<FormField
|
||||
formApi={this.props.formApi}
|
||||
label={this.props.hideLabel ? undefined : 'Revision'}
|
||||
field={this.props.fieldValue ? this.props.fieldValue : 'spec.source.targetRevision'}
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: revisions,
|
||||
filterSuggestions: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DataLoader>
|
||||
<RevisionHelpIcon type='git' top={this.props.helpIconTop} right='0em' />
|
||||
</div>
|
||||
<div style={{paddingTop: extraPadding}} className='columns small-2'>
|
||||
{this.props.repoType !== 'oci' && (
|
||||
|
||||
@@ -215,7 +215,7 @@ const PropagationPolicyOption = ReactForm.FormField((props: {fieldApi: ReactForm
|
||||
export const OperationPhaseIcon = ({app, isButton}: {app: appModels.Application; isButton?: boolean}) => {
|
||||
const operationState = getAppOperationState(app);
|
||||
if (operationState === undefined) {
|
||||
return <React.Fragment />;
|
||||
return null;
|
||||
}
|
||||
let className = '';
|
||||
let color = '';
|
||||
@@ -246,7 +246,7 @@ export const OperationPhaseIcon = ({app, isButton}: {app: appModels.Application;
|
||||
|
||||
export const HydrateOperationPhaseIcon = ({operationState, isButton}: {operationState?: appModels.HydrateOperation; isButton?: boolean}) => {
|
||||
if (operationState === undefined) {
|
||||
return <React.Fragment />;
|
||||
return null;
|
||||
}
|
||||
let className = '';
|
||||
let color = '';
|
||||
@@ -403,17 +403,15 @@ export const deleteSourceAction = (app: appModels.Application, source: appModels
|
||||
() => (
|
||||
<div>
|
||||
<p>
|
||||
<>
|
||||
Are you sure you want to delete the source with URL: <kbd>{source.repoURL}</kbd>
|
||||
{source.path ? (
|
||||
<>
|
||||
{' '}
|
||||
and path: <kbd>{source.path}</kbd>?
|
||||
</>
|
||||
) : (
|
||||
<>?</>
|
||||
)}
|
||||
</>
|
||||
Are you sure you want to delete the source with URL: <kbd>{source.repoURL}</kbd>
|
||||
{source.path ? (
|
||||
<>
|
||||
{' '}
|
||||
and path: <kbd>{source.path}</kbd>?
|
||||
</>
|
||||
) : (
|
||||
<>?</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
@@ -1145,10 +1143,10 @@ const getOperationStateTitle = (app: appModels.Application) => {
|
||||
export const OperationState = ({app, quiet, isButton}: {app: appModels.Application; quiet?: boolean; isButton?: boolean}) => {
|
||||
const appOperationState = getAppOperationState(app);
|
||||
if (appOperationState === undefined) {
|
||||
return <React.Fragment />;
|
||||
return null;
|
||||
}
|
||||
if (quiet && [appModels.OperationPhases.Running, appModels.OperationPhases.Failed, appModels.OperationPhases.Error].indexOf(appOperationState.phase) === -1) {
|
||||
return <React.Fragment />;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,33 +15,31 @@ const CustomTopBar = (props: {toolbar?: Toolbar | Observable<Toolbar>}) => {
|
||||
const ctx = React.useContext(Context);
|
||||
const loadToolbar = AddAuthToToolbar(props.toolbar, ctx);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className='top-bar row top-bar' key='tool-bar'>
|
||||
<DataLoader load={() => loadToolbar}>
|
||||
{toolbar => (
|
||||
<React.Fragment>
|
||||
<div className='column small-11 flex-top-bar_center'>
|
||||
<div className='top-bar'>
|
||||
<div className='text-center'>
|
||||
<span className='help-text'>
|
||||
Refer to CLI{' '}
|
||||
<a
|
||||
href='https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-management/#adding-a-cluster'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'>
|
||||
<i className='fa fa-external-link-alt' /> Documentation{' '}
|
||||
</a>{' '}
|
||||
for adding clusters.
|
||||
</span>
|
||||
</div>
|
||||
<div className='top-bar row top-bar' key='tool-bar'>
|
||||
<DataLoader load={() => loadToolbar}>
|
||||
{toolbar => (
|
||||
<React.Fragment>
|
||||
<div className='column small-11 flex-top-bar_center'>
|
||||
<div className='top-bar'>
|
||||
<div className='text-center'>
|
||||
<span className='help-text'>
|
||||
Refer to CLI{' '}
|
||||
<a
|
||||
href='https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-management/#adding-a-cluster'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'>
|
||||
<i className='fa fa-external-link-alt' /> Documentation{' '}
|
||||
</a>{' '}
|
||||
for adding clusters.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='columns small-1 top-bar__right-side'>{toolbar.tools}</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DataLoader>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
<div className='columns small-1 top-bar__right-side'>{toolbar.tools}</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DataLoader>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,95 +48,93 @@ export const ClustersList = () => {
|
||||
return (
|
||||
<Consumer>
|
||||
{ctx => (
|
||||
<React.Fragment>
|
||||
<Page title='Clusters' toolbar={{breadcrumbs: [{title: 'Settings', path: '/settings'}, {title: 'Clusters'}]}} hideAuth={true}>
|
||||
<CustomTopBar />
|
||||
<div className='repos-list'>
|
||||
<div className='argo-container'>
|
||||
<DataLoader
|
||||
ref={clustersLoaderRef}
|
||||
load={() => services.clusters.list().then(clusters => clusters.sort((first, second) => first.name.localeCompare(second.name)))}>
|
||||
{(clusters: models.Cluster[]) =>
|
||||
(clusters.length > 0 && (
|
||||
<div className='argo-table-list argo-table-list--clickable'>
|
||||
<div className='argo-table-list__head'>
|
||||
<div className='row'>
|
||||
<div className='columns small-3'>NAME</div>
|
||||
<div className='columns small-5'>URL</div>
|
||||
<div className='columns small-2'>VERSION</div>
|
||||
<div className='columns small-2'>CONNECTION STATUS</div>
|
||||
</div>
|
||||
<Page title='Clusters' toolbar={{breadcrumbs: [{title: 'Settings', path: '/settings'}, {title: 'Clusters'}]}} hideAuth={true}>
|
||||
<CustomTopBar />
|
||||
<div className='repos-list'>
|
||||
<div className='argo-container'>
|
||||
<DataLoader
|
||||
ref={clustersLoaderRef}
|
||||
load={() => services.clusters.list().then(clusters => clusters.sort((first, second) => first.name.localeCompare(second.name)))}>
|
||||
{(clusters: models.Cluster[]) =>
|
||||
(clusters.length > 0 && (
|
||||
<div className='argo-table-list argo-table-list--clickable'>
|
||||
<div className='argo-table-list__head'>
|
||||
<div className='row'>
|
||||
<div className='columns small-3'>NAME</div>
|
||||
<div className='columns small-5'>URL</div>
|
||||
<div className='columns small-2'>VERSION</div>
|
||||
<div className='columns small-2'>CONNECTION STATUS</div>
|
||||
</div>
|
||||
{clusters.map(cluster => (
|
||||
<div
|
||||
className='argo-table-list__row'
|
||||
key={cluster.server}
|
||||
onClick={() => ctx.navigation.goto(`./${encodeURIComponent(cluster.server)}`)}>
|
||||
<div className='row'>
|
||||
<div className='columns small-3'>
|
||||
<i className='icon argo-icon-hosts' />
|
||||
<Tooltip content={clusterName(cluster.name)}>
|
||||
<span>{clusterName(cluster.name)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='columns small-5'>
|
||||
<Tooltip content={cluster.server}>
|
||||
<span>{cluster.server}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='columns small-2'>{cluster.info.serverVersion}</div>
|
||||
<div className='columns small-2'>
|
||||
<ConnectionStateIcon state={cluster.info.connectionState} /> {cluster.info.connectionState.status}
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<button className='argo-button argo-button--light argo-button--lg argo-button--short'>
|
||||
<i className='fa fa-ellipsis-v' />
|
||||
</button>
|
||||
)}
|
||||
items={[
|
||||
{
|
||||
title: 'Delete',
|
||||
action: async () => {
|
||||
const confirmed = await ctx.popup.confirm(
|
||||
'Delete cluster?',
|
||||
`Are you sure you want to delete cluster: ${cluster.name}`
|
||||
);
|
||||
if (confirmed) {
|
||||
try {
|
||||
await services.clusters.delete(cluster.server).finally(() => {
|
||||
ctx.navigation.goto('.', {new: null}, {replace: true});
|
||||
if (clustersLoaderRef.current) {
|
||||
clustersLoaderRef.current.reload();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
ctx.notifications.show({
|
||||
content: <ErrorNotification title='Unable to delete cluster' e={e} />,
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
</div>
|
||||
{clusters.map(cluster => (
|
||||
<div
|
||||
className='argo-table-list__row'
|
||||
key={cluster.server}
|
||||
onClick={() => ctx.navigation.goto(`./${encodeURIComponent(cluster.server)}`)}>
|
||||
<div className='row'>
|
||||
<div className='columns small-3'>
|
||||
<i className='icon argo-icon-hosts' />
|
||||
<Tooltip content={clusterName(cluster.name)}>
|
||||
<span>{clusterName(cluster.name)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='columns small-5'>
|
||||
<Tooltip content={cluster.server}>
|
||||
<span>{cluster.server}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='columns small-2'>{cluster.info.serverVersion}</div>
|
||||
<div className='columns small-2'>
|
||||
<ConnectionStateIcon state={cluster.info.connectionState} /> {cluster.info.connectionState.status}
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<button className='argo-button argo-button--light argo-button--lg argo-button--short'>
|
||||
<i className='fa fa-ellipsis-v' />
|
||||
</button>
|
||||
)}
|
||||
items={[
|
||||
{
|
||||
title: 'Delete',
|
||||
action: async () => {
|
||||
const confirmed = await ctx.popup.confirm(
|
||||
'Delete cluster?',
|
||||
`Are you sure you want to delete cluster: ${cluster.name}`
|
||||
);
|
||||
if (confirmed) {
|
||||
try {
|
||||
await services.clusters.delete(cluster.server).finally(() => {
|
||||
ctx.navigation.goto('.', {new: null}, {replace: true});
|
||||
if (clustersLoaderRef.current) {
|
||||
clustersLoaderRef.current.reload();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
ctx.notifications.show({
|
||||
content: <ErrorNotification title='Unable to delete cluster' e={e} />,
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)) || (
|
||||
<EmptyState icon='argo-icon-hosts'>
|
||||
<h4>No clusters connected</h4>
|
||||
<h5>Connect more clusters using argocd CLI</h5>
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
</DataLoader>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)) || (
|
||||
<EmptyState icon='argo-icon-hosts'>
|
||||
<h4>No clusters connected</h4>
|
||||
<h5>Connect more clusters using argocd CLI</h5>
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
</DataLoader>
|
||||
</div>
|
||||
</Page>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
</Page>
|
||||
)}
|
||||
</Consumer>
|
||||
);
|
||||
|
||||
@@ -170,32 +170,30 @@ function renderJWTRow(props: ProjectRoleJWTTokensProps, ctx: ContextApis, jwToke
|
||||
const isExpired = jwToken.exp && jwToken.exp < Date.now() / 1000;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className='argo-table-list__row' key={`${jwToken.iat}`}>
|
||||
<div className='row'>
|
||||
<div className='columns small-3 project-role-jwt-tokens__id'>
|
||||
<Tooltip content={jwToken.id}>
|
||||
<span className='project-role-jwt-tokens__id-tooltip'>{jwToken.id}</span>
|
||||
</Tooltip>
|
||||
{isExpired && (
|
||||
<span title='Expired' className='project-role-jwt-tokens__expired-token'>
|
||||
Expired
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip content={issuedAt}>
|
||||
<div className='columns small-4'>{issuedAt}</div>
|
||||
</Tooltip>
|
||||
<Tooltip content={expiresAt}>
|
||||
<div className='columns small-4'>{expiresAt}</div>
|
||||
</Tooltip>
|
||||
<Tooltip content='Delete Token'>
|
||||
<div className='columns small-1'>
|
||||
<i className='fa fa-times' onClick={() => deleteJWTToken(props, jwToken.iat, ctx, jwToken.id)} style={{cursor: 'pointer'}} />
|
||||
</div>
|
||||
<div className='argo-table-list__row' key={`${jwToken.iat}`}>
|
||||
<div className='row'>
|
||||
<div className='columns small-3 project-role-jwt-tokens__id'>
|
||||
<Tooltip content={jwToken.id}>
|
||||
<span className='project-role-jwt-tokens__id-tooltip'>{jwToken.id}</span>
|
||||
</Tooltip>
|
||||
{isExpired && (
|
||||
<span title='Expired' className='project-role-jwt-tokens__expired-token'>
|
||||
Expired
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip content={issuedAt}>
|
||||
<div className='columns small-4'>{issuedAt}</div>
|
||||
</Tooltip>
|
||||
<Tooltip content={expiresAt}>
|
||||
<div className='columns small-4'>{expiresAt}</div>
|
||||
</Tooltip>
|
||||
<Tooltip content='Delete Token'>
|
||||
<div className='columns small-1'>
|
||||
<i className='fa fa-times' onClick={() => deleteJWTToken(props, jwToken.iat, ctx, jwToken.id)} style={{cursor: 'pointer'}} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -872,16 +872,14 @@ export class ReposList extends React.Component<
|
||||
/>
|
||||
</div>
|
||||
{formApi.getFormState().values.ghType === 'GitHub Enterprise' && (
|
||||
<React.Fragment>
|
||||
<div className='argo-form-row'>
|
||||
<FormField
|
||||
formApi={formApi}
|
||||
label='GitHub Enterprise Base URL (e.g. https://ghe.example.com/api/v3)'
|
||||
field='githubAppEnterpriseBaseURL'
|
||||
component={Text}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<div className='argo-form-row'>
|
||||
<FormField
|
||||
formApi={formApi}
|
||||
label='GitHub Enterprise Base URL (e.g. https://ghe.example.com/api/v3)'
|
||||
field='githubAppEnterpriseBaseURL'
|
||||
component={Text}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='argo-form-row'>
|
||||
<FormField
|
||||
|
||||
@@ -6,7 +6,7 @@ export const ClipboardText = ({text}: {text: string}) => {
|
||||
const [justClicked, setJustClicked] = useState<boolean>(false);
|
||||
|
||||
if (!text) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -113,16 +113,14 @@ export class EditablePanel<T = {}> extends React.Component<EditablePanelProps<T>
|
||||
</div>
|
||||
)}
|
||||
{this.props.collapsible && (
|
||||
<React.Fragment>
|
||||
<div className='editable-panel__collapsible-button'>
|
||||
<i
|
||||
className={`fa fa-angle-${this.state.collapsed ? 'down' : 'up'} filter__collapse`}
|
||||
onClick={() => {
|
||||
this.setState({collapsed: !this.state.collapsed});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<div className='editable-panel__collapsible-button'>
|
||||
<i
|
||||
className={`fa fa-angle-${this.state.collapsed ? 'down' : 'up'} filter__collapse`}
|
||||
onClick={() => {
|
||||
this.setState({collapsed: !this.state.collapsed});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{this.props.title && <p>{this.props.title}</p>}
|
||||
{(!this.state.edit && (
|
||||
|
||||
@@ -75,12 +75,10 @@ export const Sidebar = (props: SidebarProps) => {
|
||||
key={item.title}
|
||||
className={`sidebar__nav-item ${locationPath === item.path || locationPath.startsWith(`${item.path}/`) ? 'sidebar__nav-item--active' : ''}`}
|
||||
onClick={() => context.history.push(item.path)}>
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<i className={item?.iconClassName || ''} />
|
||||
{!props.pref.hideSidebar && item.title}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<div>
|
||||
<i className={item?.iconClassName || ''} />
|
||||
{!props.pref.hideSidebar && item.title}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
@@ -41,9 +41,7 @@ const CustomBanner = (props: {
|
||||
Don't show again
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user