feat: Support applications with cluster name in the ui #1548 (#3944)

* feat: Support applications with cluster name in the ui

* Retrigger CI pipeline
This commit is contained in:
Liviu Costea 2020-07-25 01:37:58 +03:00 committed by GitHub
parent 7a348f786b
commit 275daa7976
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 49 additions and 20 deletions

View file

@ -13,14 +13,15 @@ import (
func TestClusterList(t *testing.T) {
output := FailOnErr(RunCli("cluster", "list")).(string)
assert.Equal(t, fmt.Sprintf(`SERVER NAME VERSION STATUS MESSAGE
https://kubernetes.default.svc %v Successful `, GetVersions().ServerVersion), output)
assert.Equal(t, fmt.Sprintf(`SERVER NAME VERSION STATUS MESSAGE
https://kubernetes.default.svc in-cluster %v Successful `, GetVersions().ServerVersion), output)
}
func TestClusterGet(t *testing.T) {
output := FailOnErr(RunCli("cluster", "get", "https://kubernetes.default.svc")).(string)
assert.Contains(t, output, fmt.Sprintf(`
name: in-cluster
server: https://kubernetes.default.svc
serverVersion: "%v"`, GetVersions().ServerVersion))

View file

@ -28,6 +28,7 @@ const DEFAULT_APP: Partial<models.Application> = {
},
spec: {
destination: {
name: '',
namespace: '',
server: ''
},

View file

@ -28,6 +28,8 @@ function swap(array: any[], a: number, b: number) {
export const ApplicationSummary = (props: {app: models.Application; updateApp: (app: models.Application) => Promise<any>}) => {
const app = JSON.parse(JSON.stringify(props.app)) as models.Application;
const isHelm = app.spec.source.hasOwnProperty('chart');
const isDestName = app.spec.destination.server === undefined;
const fieldDest = isDestName ? 'spec.destination.name' : 'spec.destination.server';
const attributes = [
{
@ -59,18 +61,18 @@ export const ApplicationSummary = (props: {app: models.Application; updateApp: (
},
{
title: 'CLUSTER',
view: <Cluster server={app.spec.destination.server} showUrl={true} />,
view: <Cluster server={app.spec.destination.server} name={app.spec.destination.name} showUrl={true} />,
edit: (formApi: FormApi) => (
<DataLoader
load={() =>
services.clusters.list().then(clusters =>
clusters.map(cluster => ({
title: clusterTitle(cluster),
value: cluster.server
value: isDestName ? cluster.name : cluster.server
}))
)
}>
{clusters => <FormField formApi={formApi} field='spec.destination.server' componentProps={{options: clusters}} component={FormSelect} />}
{clusters => <FormField formApi={formApi} field={fieldDest} componentProps={{options: clusters}} component={FormSelect} />}
</DataLoader>
)
},
@ -326,10 +328,17 @@ Default is 10.
<div className='application-summary'>
<EditablePanel
save={props.updateApp}
validate={input => ({
'spec.project': !input.spec.project && 'Project name is required',
'spec.destination.server': !input.spec.destination.server && 'Cluster is required'
})}
validate={input =>
isDestName
? {
'spec.project': !input.spec.project && 'Project name is required',
'spec.destination.name': !input.spec.destination.name && 'Cluster is required'
}
: {
'spec.project': !input.spec.project && 'Project name is required',
'spec.destination.server': !input.spec.destination.server && 'Cluster is required'
}
}
values={app}
title={app.metadata.name.toLocaleUpperCase()}
items={attributes}

View file

@ -18,6 +18,13 @@ export interface ApplicationTilesProps {
deleteApplication: (appName: string) => any;
}
function getDestination(dest: models.ApplicationDestination) {
if (dest.server === undefined) {
return dest.name;
}
return dest.server;
}
export const ApplicationTiles = ({applications, syncApplication, refreshApplication, deleteApplication}: ApplicationTilesProps) => (
<Consumer>
{ctx => (
@ -102,7 +109,7 @@ export const ApplicationTiles = ({applications, syncApplication, refreshApplicat
)}
<div className='row'>
<div className='columns small-3'>Destination:</div>
<div className='columns small-9'>{app.spec.destination.server}</div>
<div className='columns small-9'>{getDestination(app.spec.destination)}</div>
</div>
<div className='row'>
<div className='columns small-3'>Namespace:</div>

View file

@ -24,7 +24,7 @@ export const ClusterDetails = (props: RouteComponentProps<{server: string}>) =>
const loaderRef = React.useRef<DataLoader>();
const [updating, setUpdating] = React.useState(false);
return (
<DataLoader ref={loaderRef} input={server} load={(url: string) => Observable.interval(1000).flatMap(() => Observable.fromPromise(services.clusters.get(url)))}>
<DataLoader ref={loaderRef} input={server} load={(url: string) => Observable.interval(1000).flatMap(() => Observable.fromPromise(services.clusters.get(url, '')))}>
{(cluster: Cluster) => (
<Page
title={server}
@ -55,7 +55,7 @@ export const ClusterDetails = (props: RouteComponentProps<{server: string}>) =>
<EditablePanel
values={cluster}
save={async updated => {
const item = await services.clusters.get(updated.server);
const item = await services.clusters.get(updated.server, '');
item.name = updated.name;
item.namespaces = updated.namespaces;
loaderRef.current.setData(await services.clusters.update(item));

View file

@ -21,13 +21,13 @@ const clusterHTML = (cluster: models.Cluster, showUrl: boolean) => {
);
};
async function getCluster(clusters: Promise<models.Cluster[]>, server: string): Promise<models.Cluster> {
async function getCluster(clusters: Promise<models.Cluster[]>, server: string, name: string): Promise<models.Cluster> {
let cluster: models.Cluster;
if (clusters) {
cluster = await clusters.then(items => items.find(item => item.server === server));
cluster = await clusters.then(items => items.find(item => item.server === server || item.name === name));
} else {
try {
cluster = await services.clusters.get(server);
cluster = await services.clusters.get(server, name);
} catch {
cluster = null;
}
@ -43,10 +43,10 @@ async function getCluster(clusters: Promise<models.Cluster[]>, server: string):
export const ClusterCtx = React.createContext<Promise<Array<models.Cluster>>>(null);
export const Cluster = (props: {server: string; showUrl?: boolean}) => (
export const Cluster = (props: {server: string; name?: string; showUrl?: boolean}) => (
<ClusterCtx.Consumer>
{clusters => (
<DataLoader input={props.server} loadingRenderer={() => <span>{props.server}</span>} load={server => getCluster(clusters, server)}>
<DataLoader input={props} loadingRenderer={() => <span>{props.server}</span>} load={input => getCluster(clusters, input.server, input.name)}>
{(cluster: models.Cluster) => clusterHTML(cluster, props.showUrl)}
</DataLoader>
)}

View file

@ -142,6 +142,10 @@ export interface ApplicationDestination {
* Namespace overrides the environment namespace value in the ksonnet app.yaml
*/
namespace: string;
/**
* Name of the destination cluster which can be used instead of server (url) field
*/
name: string;
}
export interface OrphanedResource {

View file

@ -9,8 +9,14 @@ export class ClustersService {
.then(list => list.items || []);
}
public get(url: string): Promise<models.Cluster> {
return requests.get(`/clusters/${encodeURIComponent(url)}`).then(res => res.body as models.Cluster);
public get(url: string, name: string): Promise<models.Cluster> {
let queryName = '';
if (url === undefined) {
url = '';
queryName = `?name=${name}`;
}
const requestUrl = `/clusters/${encodeURIComponent(url)}` + queryName;
return requests.get(requestUrl).then(res => res.body as models.Cluster);
}
public update(cluster: models.Cluster): Promise<models.Cluster> {

View file

@ -29,6 +29,7 @@ import (
var (
localCluster = appv1.Cluster{
Name: "in-cluster",
Server: common.KubernetesInternalAPIServerAddr,
ConnectionState: appv1.ConnectionState{Status: appv1.ConnectionStatusSuccessful},
}

View file

@ -112,7 +112,7 @@ func TestWatchClusters_LocalClusterModifications(t *testing.T) {
},
func(old *v1alpha1.Cluster, new *v1alpha1.Cluster) {
assert.Equal(t, new.Server, common.KubernetesInternalAPIServerAddr)
assert.Equal(t, new.Name, "")
assert.Equal(t, new.Name, "in-cluster")
},
})
}