Add initial kubernetes services and configmap UI support (#2635)
* add initial kubernetes services and configmap support * revert accidental change * missed revert * prettier
This commit is contained in:
@@ -88,6 +88,26 @@ spec:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 9376
|
||||
name: port1
|
||||
- protocol: TCP
|
||||
port: 81
|
||||
targetPort: 9377
|
||||
name: port2
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dice-roller-lb
|
||||
labels:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
spec:
|
||||
selector:
|
||||
app: dice-roller
|
||||
ports:
|
||||
- port: 8765
|
||||
targetPort: 9376
|
||||
type: LoadBalancer
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
|
||||
@@ -34,7 +34,6 @@ const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
|
||||
'pods',
|
||||
'services',
|
||||
'configmaps',
|
||||
'secrets',
|
||||
'deployments',
|
||||
'replicasets',
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { ConfigMaps } from './ConfigMaps';
|
||||
import * as configmapFixture from './__fixtures__/configmap.json';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('ConfigMaps', () => {
|
||||
it('should render configmap', async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<ConfigMaps configMaps={(configmapFixture as any).default} />,
|
||||
),
|
||||
);
|
||||
|
||||
// title
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('ConfigMap')).toBeInTheDocument();
|
||||
|
||||
// values
|
||||
expect(getByText('Immutable')).toBeInTheDocument();
|
||||
expect(getByText('false')).toBeInTheDocument();
|
||||
expect(getByText('Data')).toBeInTheDocument();
|
||||
expect(getByText('Foo: bar')).toBeInTheDocument(); // TODO wish this wasn't upper case
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { V1ConfigMap } from '@kubernetes/client-node';
|
||||
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
|
||||
|
||||
type ConfigMapsProps = {
|
||||
configMaps: V1ConfigMap[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ConfigMaps = ({ configMaps }: ConfigMapsProps) => {
|
||||
return (
|
||||
<Grid container>
|
||||
{configMaps.map((cm, i) => {
|
||||
return (
|
||||
<Grid item key={i}>
|
||||
<InfoCard
|
||||
title={cm.metadata?.name ?? 'un-named service'}
|
||||
subheader="ConfigMap"
|
||||
>
|
||||
<div>
|
||||
<StructuredMetadataTable
|
||||
metadata={{
|
||||
immutable: `${cm.immutable === true}`,
|
||||
data: cm.data,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"foo\":\"bar\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"}}\n"
|
||||
},
|
||||
"creationTimestamp": "2020-09-24T11:39:26.000Z",
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:data": {
|
||||
".": {},
|
||||
"f:foo": {}
|
||||
},
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
".": {},
|
||||
"f:kubectl.kubernetes.io/last-applied-configuration": {}
|
||||
},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:backstage.io/kubernetes-id": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"manager": "kubectl",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-24T11:39:26.000Z"
|
||||
}
|
||||
],
|
||||
"name": "dice-roller",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "503867",
|
||||
"selfLink": "/api/v1/namespaces/default/configmaps/dice-roller",
|
||||
"uid": "e9efe5ee-53b9-4422-aef2-877a03c73d5f"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { ConfigMaps } from './ConfigMaps';
|
||||
@@ -17,23 +17,33 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import {
|
||||
CardTab,
|
||||
Content,
|
||||
InfoCard,
|
||||
Page,
|
||||
pageTheme,
|
||||
Progress,
|
||||
TabbedCard,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { kubernetesApiRef } from '../../api/types';
|
||||
import {
|
||||
ClusterObjects,
|
||||
FetchResponse,
|
||||
ObjectsByServiceIdResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
import { DeploymentTables } from '../DeploymentTables';
|
||||
import { DeploymentTriple } from '../../types/types';
|
||||
import { V1ConfigMap, V1Service } from '@kubernetes/client-node';
|
||||
import { Services } from '../Services';
|
||||
import { ConfigMaps } from '../ConfigMaps';
|
||||
|
||||
const findDeployments = (fetchResponse: FetchResponse[]): DeploymentTriple => {
|
||||
interface GroupedResponses extends DeploymentTriple {
|
||||
services: V1Service[];
|
||||
configMaps: V1ConfigMap[];
|
||||
}
|
||||
|
||||
const groupResponses = (fetchResponse: FetchResponse[]) => {
|
||||
return fetchResponse.reduce(
|
||||
(prev, next) => {
|
||||
switch (next.type) {
|
||||
@@ -46,6 +56,12 @@ const findDeployments = (fetchResponse: FetchResponse[]): DeploymentTriple => {
|
||||
case 'replicasets':
|
||||
prev.replicaSets.push(...next.resources);
|
||||
break;
|
||||
case 'services':
|
||||
prev.services.push(...next.resources);
|
||||
break;
|
||||
case 'configmaps':
|
||||
prev.configMaps.push(...next.resources);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return prev;
|
||||
@@ -54,7 +70,9 @@ const findDeployments = (fetchResponse: FetchResponse[]): DeploymentTriple => {
|
||||
pods: [],
|
||||
replicaSets: [],
|
||||
deployments: [],
|
||||
} as DeploymentTriple,
|
||||
services: [],
|
||||
configMaps: [],
|
||||
} as GroupedResponses,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -89,11 +107,7 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
{error !== undefined && <div>{error}</div>}
|
||||
{kubernetesObjects?.items.map((item, i) => (
|
||||
<Grid item key={i}>
|
||||
<InfoCard title={item.cluster.name} subheader="Cluster">
|
||||
<DeploymentTables
|
||||
deploymentTriple={findDeployments(item.resources)}
|
||||
/>
|
||||
</InfoCard>
|
||||
<Cluster clusterObjects={item} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
@@ -101,3 +115,47 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
type ClusterProps = {
|
||||
clusterObjects: ClusterObjects;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Cluster = ({ clusterObjects }: ClusterProps) => {
|
||||
const [selectedTab, setSelectedTab] = useState<string | number>('one');
|
||||
|
||||
const handleChange = (_ev: any, newSelectedTab: string | number) =>
|
||||
setSelectedTab(newSelectedTab);
|
||||
|
||||
const groupedResponses = groupResponses(clusterObjects.resources);
|
||||
|
||||
const configMaps = groupedResponses.configMaps;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabbedCard
|
||||
value={selectedTab}
|
||||
onChange={handleChange}
|
||||
title={clusterObjects.cluster.name}
|
||||
>
|
||||
<CardTab value="one" label="Deployments">
|
||||
<DeploymentTables
|
||||
deploymentTriple={{
|
||||
deployments: groupedResponses.deployments,
|
||||
replicaSets: groupedResponses.replicaSets,
|
||||
pods: groupedResponses.pods,
|
||||
}}
|
||||
/>
|
||||
</CardTab>
|
||||
<CardTab value="two" label="Services">
|
||||
<Services services={groupedResponses.services} />
|
||||
</CardTab>
|
||||
{configMaps && (
|
||||
<CardTab value="three" label="ConfigMaps">
|
||||
<ConfigMaps configMaps={configMaps} />
|
||||
</CardTab>
|
||||
)}
|
||||
</TabbedCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Services } from './Services';
|
||||
import * as servicesFixture from './__fixtures__/services.json';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('Services', () => {
|
||||
it('should render 2 services', async () => {
|
||||
const { getByText, getAllByText } = render(
|
||||
wrapInTestApp(<Services services={(servicesFixture as any).default} />),
|
||||
);
|
||||
|
||||
// common elements
|
||||
expect(getAllByText('Service')).toHaveLength(2);
|
||||
expect(getAllByText('Ports')).toHaveLength(2);
|
||||
expect(getAllByText('Type')).toHaveLength(2);
|
||||
expect(getAllByText('Protocol: TCP')).toHaveLength(3);
|
||||
|
||||
// service 1
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('ClusterIP')).toBeInTheDocument();
|
||||
|
||||
expect(getByText('Name: port1')).toBeInTheDocument();
|
||||
expect(getByText('Port: 80')).toBeInTheDocument();
|
||||
expect(getByText('Target Port: 9376')).toBeInTheDocument();
|
||||
expect(getByText('Name: port1')).toBeInTheDocument();
|
||||
expect(getByText('Port: 81')).toBeInTheDocument();
|
||||
expect(getByText('Target Port: 9377')).toBeInTheDocument();
|
||||
expect(getByText('10.102.223.105')).toBeInTheDocument();
|
||||
|
||||
// service 2
|
||||
expect(getByText('dice-roller-lb')).toBeInTheDocument();
|
||||
expect(getByText('LoadBalancer')).toBeInTheDocument();
|
||||
expect(getByText('Node Port: 32276')).toBeInTheDocument();
|
||||
expect(getByText('Port: 8765')).toBeInTheDocument();
|
||||
expect(getByText('Target Port: 9378')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { V1Service } from '@kubernetes/client-node';
|
||||
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
|
||||
|
||||
type ServicesProps = {
|
||||
services: V1Service[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Services = ({ services }: ServicesProps) => {
|
||||
return (
|
||||
<Grid container>
|
||||
{services.map((s, i) => {
|
||||
const metadata: any = {};
|
||||
|
||||
if (s.status?.loadBalancer?.ingress?.length ?? -1 > 0) {
|
||||
metadata.loadbalancer = s.status?.loadBalancer;
|
||||
}
|
||||
|
||||
if (s.spec?.type === 'ClusterIP') {
|
||||
metadata.clusterIP = s.spec.clusterIP;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid item key={i}>
|
||||
<InfoCard
|
||||
title={s.metadata?.name ?? 'un-named service'}
|
||||
subheader="Service"
|
||||
>
|
||||
<div>
|
||||
<StructuredMetadataTable
|
||||
metadata={{
|
||||
type: s.spec?.type,
|
||||
ports: s.spec?.ports,
|
||||
...metadata,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
[
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"name\":\"port1\",\"port\":80,\"protocol\":\"TCP\",\"targetPort\":9376},{\"name\":\"port2\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":9377}],\"selector\":{\"app\":\"dice-roller\"}}}\n"
|
||||
},
|
||||
"creationTimestamp": "2020-09-23T12:00:55.000Z",
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
".": {},
|
||||
"f:kubectl.kubernetes.io/last-applied-configuration": {}
|
||||
},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:backstage.io/kubernetes-id": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
"f:ports": {
|
||||
".": {},
|
||||
"k:{\"port\":80,\"protocol\":\"TCP\"}": {
|
||||
".": {},
|
||||
"f:name": {},
|
||||
"f:port": {},
|
||||
"f:protocol": {},
|
||||
"f:targetPort": {}
|
||||
},
|
||||
"k:{\"port\":81,\"protocol\":\"TCP\"}": {
|
||||
".": {},
|
||||
"f:name": {},
|
||||
"f:port": {},
|
||||
"f:protocol": {},
|
||||
"f:targetPort": {}
|
||||
}
|
||||
},
|
||||
"f:selector": {
|
||||
".": {},
|
||||
"f:app": {}
|
||||
},
|
||||
"f:sessionAffinity": {},
|
||||
"f:type": {}
|
||||
}
|
||||
},
|
||||
"manager": "kubectl",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-28T08:50:11.000Z"
|
||||
}
|
||||
],
|
||||
"name": "dice-roller",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "665838",
|
||||
"selfLink": "/api/v1/namespaces/default/services/dice-roller",
|
||||
"uid": "ae9aff92-a525-4bc9-82dc-a0537bf8034c"
|
||||
},
|
||||
"spec": {
|
||||
"clusterIP": "10.102.223.105",
|
||||
"ports": [
|
||||
{
|
||||
"name": "port1",
|
||||
"port": 80,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 9376
|
||||
},
|
||||
{
|
||||
"name": "port2",
|
||||
"port": 81,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 9377
|
||||
}
|
||||
],
|
||||
"selector": {
|
||||
"app": "dice-roller"
|
||||
},
|
||||
"sessionAffinity": "None",
|
||||
"type": "ClusterIP"
|
||||
},
|
||||
"status": {
|
||||
"loadBalancer": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller-lb\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"port\":8765,\"targetPort\":9376}],\"selector\":{\"app\":\"dice-roller\"},\"type\":\"LoadBalancer\"}}\n"
|
||||
},
|
||||
"creationTimestamp": "2020-09-28T08:51:21.000Z",
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
".": {},
|
||||
"f:kubectl.kubernetes.io/last-applied-configuration": {}
|
||||
},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:backstage.io/kubernetes-id": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
"f:externalTrafficPolicy": {},
|
||||
"f:ports": {
|
||||
".": {},
|
||||
"k:{\"port\":8765,\"protocol\":\"TCP\"}": {
|
||||
".": {},
|
||||
"f:port": {},
|
||||
"f:protocol": {},
|
||||
"f:targetPort": {}
|
||||
}
|
||||
},
|
||||
"f:selector": {
|
||||
".": {},
|
||||
"f:app": {}
|
||||
},
|
||||
"f:sessionAffinity": {},
|
||||
"f:type": {}
|
||||
}
|
||||
},
|
||||
"manager": "kubectl",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-28T08:51:21.000Z"
|
||||
}
|
||||
],
|
||||
"name": "dice-roller-lb",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "665998",
|
||||
"selfLink": "/api/v1/namespaces/default/services/dice-roller-lb",
|
||||
"uid": "5554da3b-2041-4403-8cf4-cd2ccae760f8"
|
||||
},
|
||||
"spec": {
|
||||
"clusterIP": "10.99.205.233",
|
||||
"externalTrafficPolicy": "Cluster",
|
||||
"ports": [
|
||||
{
|
||||
"nodePort": 32276,
|
||||
"port": 8765,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 9378
|
||||
}
|
||||
],
|
||||
"selector": {
|
||||
"app": "dice-roller"
|
||||
},
|
||||
"sessionAffinity": "None",
|
||||
"type": "LoadBalancer"
|
||||
},
|
||||
"status": {
|
||||
"loadBalancer": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { Services } from './Services';
|
||||
Reference in New Issue
Block a user