chore(ui): add jsx-no-useless-fragment lint rule (#23666)

Signed-off-by: linghaoSu <linghao.su@daocloud.io>
This commit is contained in:
Linghao Su
2025-07-07 02:45:39 +08:00
committed by GitHub
parent 09b5cbdda2
commit d58ba040e9
26 changed files with 1047 additions and 1103 deletions

View File

@@ -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,

View File

@@ -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>
);
};

View File

@@ -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>
);
});

View File

@@ -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>
);
};

View File

@@ -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 && (
<>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>
);
};

View File

@@ -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}>

View File

@@ -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 && (

View File

@@ -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 (

View File

@@ -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]} &nbsp;
</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]} &nbsp;
</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',

View File

@@ -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>

View File

@@ -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>

View File

@@ -71,7 +71,7 @@ export const FiltersGroup = (props: {
</button>
</div>
)}
<>{props.children}</>
{props.children}
<div className='filters-group__content'>{props.content}</div>
</div>
)

View File

@@ -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')}>

View File

@@ -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

View File

@@ -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' && (

View File

@@ -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 (

View File

@@ -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>
);

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -6,7 +6,7 @@ export const ClipboardText = ({text}: {text: string}) => {
const [justClicked, setJustClicked] = useState<boolean>(false);
if (!text) {
return <></>;
return null;
}
return (

View File

@@ -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 && (

View File

@@ -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>
))}

View File

@@ -41,9 +41,7 @@ const CustomBanner = (props: {
Don't show again
</button>
</>
) : (
<></>
)}
) : null}
</div>
);
};