Merge branch 'master' of github.com:Nek/backstage
This commit is contained in:
@@ -14,13 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { serveBundle } from '../../lib/bundler';
|
||||
import { Command } from 'commander';
|
||||
import { paths } from '../../lib/paths';
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
const pkgPath = paths.resolveTarget('package.json');
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
|
||||
const waitForExit = await serveBundle({
|
||||
entry: 'dev/index',
|
||||
checksEnabled: cmd.check,
|
||||
proxy: pkg.proxy,
|
||||
});
|
||||
|
||||
await waitForExit();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import yn from 'yn';
|
||||
import webpack from 'webpack';
|
||||
import WebpackDevServer from 'webpack-dev-server';
|
||||
@@ -36,6 +37,8 @@ export async function serveBundle(options: ServeOptions) {
|
||||
const urls = prepareUrls(protocol, host, port);
|
||||
|
||||
const paths = resolveBundlingPaths(options);
|
||||
const pkgPath = paths.targetPackageJson;
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
const config = createConfig(paths, { ...options, isDev: true });
|
||||
const compiler = webpack(config);
|
||||
|
||||
@@ -48,7 +51,7 @@ export async function serveBundle(options: ServeOptions) {
|
||||
https: protocol === 'https',
|
||||
host,
|
||||
port,
|
||||
proxy: options.proxy,
|
||||
proxy: pkg.proxy,
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 { createDevApp } from '@backstage/dev-utils';
|
||||
import { plugin } from '../src/plugin';
|
||||
import { circleCIApiRef, CircleCIApi } from '../src/api';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(plugin)
|
||||
.registerApiFactory({
|
||||
deps: {},
|
||||
factory: () => new CircleCIApi(),
|
||||
implements: circleCIApiRef,
|
||||
})
|
||||
.render();
|
||||
@@ -3,18 +3,31 @@
|
||||
"version": "0.1.1-alpha.4",
|
||||
"main": "dist/index.esm.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"proxy": {
|
||||
"/circleci/api": {
|
||||
"target": "https://circleci.com/api/v1.1",
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/circleci/api/": "/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean"
|
||||
"clean": "backstage-cli clean",
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"start": "backstage-cli plugin:serve",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.4",
|
||||
"@backstage/theme": "^0.1.1-alpha.4",
|
||||
"@backstage/core": "^0.1.1-alpha.5",
|
||||
"@backstage/theme": "^0.1.1-alpha.5",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -22,24 +35,25 @@
|
||||
"circleci-api": "^4.0.0",
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"moment": "^2.25.3",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-lazylog": "^4.5.2",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-use": "^13.0.0"
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@backstage/cli": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "5.0.2",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.5"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist/**/*.{js,d.ts}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -48,8 +48,15 @@ export class CircleCIApi {
|
||||
});
|
||||
}
|
||||
|
||||
async getBuilds(options: CircleCIOptions) {
|
||||
async getBuilds(
|
||||
{ limit = 10, offset = 0 }: { limit: number; offset: number },
|
||||
options: CircleCIOptions,
|
||||
) {
|
||||
return getBuildSummaries(options.token, {
|
||||
options: {
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
vcs: {},
|
||||
circleHost: this.apiUrl,
|
||||
...options,
|
||||
|
||||
@@ -16,20 +16,24 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import { BuildsPage } from '../pages/BuildsPage';
|
||||
import { SettingsPage } from '../pages/SettingsPage';
|
||||
import { DetailedViewPage } from '../pages/BuildWithStepsPage';
|
||||
import { AppStateProvider } from '../state';
|
||||
import { Settings } from './Settings';
|
||||
|
||||
export const App = () => (
|
||||
<AppStateProvider>
|
||||
<Switch>
|
||||
<Route path="/circleci" exact component={BuildsPage} />
|
||||
<Route path="/circleci/settings" exact component={SettingsPage} />
|
||||
<Route
|
||||
path="/circleci/build/:buildId"
|
||||
exact
|
||||
component={DetailedViewPage}
|
||||
/>
|
||||
</Switch>
|
||||
</AppStateProvider>
|
||||
);
|
||||
export const App = () => {
|
||||
return (
|
||||
<AppStateProvider>
|
||||
<>
|
||||
<Switch>
|
||||
<Route path="/circleci" exact component={BuildsPage} />
|
||||
<Route
|
||||
path="/circleci/build/:buildId"
|
||||
exact
|
||||
component={DetailedViewPage}
|
||||
/>
|
||||
</Switch>
|
||||
<Settings />
|
||||
</>
|
||||
</AppStateProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,20 +17,22 @@ import React from 'react';
|
||||
import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core';
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
export const Layout: React.FC = ({ children }) => (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
pageTitleOverride="Circle CI"
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<img src={logo} style={{ height: '1em' }} alt="Backstage Logo" />
|
||||
<Box mr={1} /> Circle CI
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
export const Layout: React.FC = ({ children }) => {
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
pageTitleOverride="Circle CI"
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<img src={logo} style={{ height: '1em' }} alt="Backstage Logo" />
|
||||
<Box mr={1} /> Circle CI
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,9 +19,11 @@ import { ContentHeader, SupportButton } from '@backstage/core';
|
||||
import { Button, IconButton, Box, Typography } from '@material-ui/core';
|
||||
import ArrowBack from '@material-ui/icons/ArrowBack';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import { useSettings } from '../../state';
|
||||
|
||||
export type Props = { title?: string };
|
||||
export const PluginHeader: FC<Props> = ({ title = 'Circle CI' }) => {
|
||||
const [, { showSettings }] = useSettings();
|
||||
const location = useLocation();
|
||||
const notRoot = !location.pathname.match(/\/circleci\/?$/);
|
||||
const isSettingsPage = location.pathname.match(/\/circleci\/settings\/?/);
|
||||
@@ -40,11 +42,7 @@ export const PluginHeader: FC<Props> = ({ title = 'Circle CI' }) => {
|
||||
)}
|
||||
>
|
||||
{!isSettingsPage && (
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/circleci/settings"
|
||||
startIcon={<SettingsIcon />}
|
||||
>
|
||||
<Button onClick={showSettings} startIcon={<SettingsIcon />}>
|
||||
Settings
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
List,
|
||||
ListItem,
|
||||
Snackbar,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
} from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useSettings } from '../../state';
|
||||
|
||||
const Settings = () => {
|
||||
const [
|
||||
{
|
||||
repo: repoFromStore,
|
||||
owner: ownerFromStore,
|
||||
token: tokenFromStore,
|
||||
showSettings,
|
||||
},
|
||||
{ saveSettings, hideSettings },
|
||||
] = useSettings();
|
||||
|
||||
const [token, setToken] = React.useState(() => tokenFromStore);
|
||||
const [owner, setOwner] = React.useState(() => ownerFromStore);
|
||||
const [repo, setRepo] = React.useState(() => repoFromStore);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tokenFromStore !== token) {
|
||||
setToken(tokenFromStore);
|
||||
}
|
||||
if (ownerFromStore !== owner) {
|
||||
setOwner(ownerFromStore);
|
||||
}
|
||||
if (repoFromStore !== repo) {
|
||||
setRepo(repoFromStore);
|
||||
}
|
||||
}, [ownerFromStore, repoFromStore, tokenFromStore]);
|
||||
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar
|
||||
autoHideDuration={1000}
|
||||
open={saved}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
onClose={() => setSaved(false)}
|
||||
>
|
||||
<Alert severity="success">Credentials saved.</Alert>
|
||||
</Snackbar>
|
||||
<Dialog open={showSettings} onClose={hideSettings}>
|
||||
<DialogTitle>
|
||||
Project Credentials
|
||||
{/* {authed ? <StatusOK /> : <StatusFailed />} */}
|
||||
</DialogTitle>
|
||||
<Box minWidth="400px">
|
||||
<List>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-token"
|
||||
label="Token"
|
||||
value={token}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-owner"
|
||||
fullWidth
|
||||
label="Owner"
|
||||
variant="outlined"
|
||||
value={owner}
|
||||
onChange={(e) => setOwner(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-repo"
|
||||
label="Repo"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={repo}
|
||||
onChange={(e) => setRepo(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Box mt={2} display="flex" width="100%" justifyContent="center">
|
||||
<Button
|
||||
data-testid="github-auth-button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setSaved(true);
|
||||
saveSettings({ repo, owner, token });
|
||||
hideSettings();
|
||||
}}
|
||||
>
|
||||
Save credentials
|
||||
</Button>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
+1
-1
@@ -13,4 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { default as SettingsPage } from './SettingsPage';
|
||||
export { default as Settings } from './Settings';
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Content, InfoCard } from '@backstage/core';
|
||||
import { Content, InfoCard, Progress } from '@backstage/core';
|
||||
import { BuildWithSteps, BuildStepAction } from '../../api';
|
||||
import { Grid, Box, Link, IconButton } from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
@@ -27,7 +27,7 @@ import { useSettings } from '../../state/useSettings';
|
||||
import { useBuildWithSteps } from '../../state/useBuildWithSteps';
|
||||
|
||||
const IconLink = IconButton as typeof Link;
|
||||
const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
|
||||
const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
#{build?.build_num} - {build?.subject}
|
||||
<IconLink href={build?.build_url} target="_blank">
|
||||
@@ -96,7 +96,7 @@ const BuildWithStepsPage: FC<{}> = () => {
|
||||
const { buildId = '' } = useParams();
|
||||
const classes = useStyles();
|
||||
const [settings] = useSettings();
|
||||
const [build, { startPolling, stopPolling }] = useBuildWithSteps(
|
||||
const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps(
|
||||
parseInt(buildId, 10),
|
||||
);
|
||||
|
||||
@@ -113,11 +113,11 @@ const BuildWithStepsPage: FC<{}> = () => {
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard
|
||||
className={pickClassName(classes, build)}
|
||||
title={<BuildName build={build} />}
|
||||
className={pickClassName(classes, value)}
|
||||
title={<BuildName build={value} />}
|
||||
cardClassName={classes.cardContent}
|
||||
>
|
||||
<BuildsList build={build} />
|
||||
{loading ? <Progress /> : <BuildsList build={value} />}
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -126,7 +126,7 @@ const BuildWithStepsPage: FC<{}> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
|
||||
const BuildsList: FC<{ build?: BuildWithSteps }> = ({ build }) => (
|
||||
<Box>
|
||||
{build &&
|
||||
build.steps &&
|
||||
|
||||
@@ -13,74 +13,26 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { CITableBuildInfo, CITable } from '../CITable';
|
||||
import { BuildSummary } from '../../../../api';
|
||||
import { useBuilds } from '../../../../state/useBuilds';
|
||||
import { useSettings } from '../../../../state/useSettings';
|
||||
|
||||
const makeReadableStatus = (status: string | undefined) => {
|
||||
if (!status) return '';
|
||||
return ({
|
||||
retried: 'Retried',
|
||||
canceled: 'Canceled',
|
||||
infrastructure_fail: 'Infra fail',
|
||||
timedout: 'Timedout',
|
||||
not_run: 'Not run',
|
||||
running: 'Running',
|
||||
failed: 'Failed',
|
||||
queued: 'Queued',
|
||||
scheduled: 'Scheduled',
|
||||
not_running: 'Not running',
|
||||
no_tests: 'No tests',
|
||||
fixed: 'Fixed',
|
||||
success: 'Success',
|
||||
} as Record<string, string>)[status];
|
||||
};
|
||||
|
||||
const transform = (
|
||||
buildsData: BuildSummary[],
|
||||
restartBuild: { (buildId: number): Promise<void> },
|
||||
): CITableBuildInfo[] => {
|
||||
return buildsData.map((buildData) => {
|
||||
const tableBuildInfo: CITableBuildInfo = {
|
||||
id: String(buildData.build_num),
|
||||
buildName: buildData.subject
|
||||
? buildData.subject +
|
||||
(buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '')
|
||||
: '',
|
||||
onRestartClick: () =>
|
||||
typeof buildData.build_num !== 'undefined' &&
|
||||
restartBuild(buildData.build_num),
|
||||
source: {
|
||||
branchName: String(buildData.branch),
|
||||
commit: {
|
||||
hash: String(buildData.vcs_revision),
|
||||
url: 'todo',
|
||||
},
|
||||
},
|
||||
status: makeReadableStatus(buildData.status),
|
||||
buildUrl: buildData.build_url,
|
||||
};
|
||||
return tableBuildInfo;
|
||||
});
|
||||
};
|
||||
import React, { FC } from 'react';
|
||||
import { CITable } from '../CITable';
|
||||
import { useBuilds } from '../../../../state';
|
||||
|
||||
export const Builds: FC<{}> = () => {
|
||||
const [
|
||||
builds,
|
||||
{ restartBuild: handleRestartBuild, startPolling, stopPolling },
|
||||
{ total, loading, value, projectName, page, pageSize },
|
||||
{ setPage, retry, setPageSize },
|
||||
] = useBuilds();
|
||||
const [{ repo, owner }] = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
startPolling();
|
||||
return () => stopPolling();
|
||||
}, [repo, owner]);
|
||||
|
||||
const transformedBuilds = transform(builds, handleRestartBuild);
|
||||
|
||||
return (
|
||||
<CITable builds={transformedBuilds} projectName={`${owner}/${repo}`} />
|
||||
<CITable
|
||||
total={total}
|
||||
loading={loading}
|
||||
retry={retry}
|
||||
builds={value ?? []}
|
||||
projectName={projectName}
|
||||
page={page}
|
||||
onChangePage={setPage}
|
||||
pageSize={pageSize}
|
||||
onChangePageSize={setPageSize}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -114,14 +114,46 @@ const generatedColumns: TableColumn[] = [
|
||||
width: '10%',
|
||||
},
|
||||
];
|
||||
export const CITable: FC<{
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
retry: () => void;
|
||||
builds: CITableBuildInfo[];
|
||||
projectName: string;
|
||||
}> = React.memo(({ builds = [], projectName }) => {
|
||||
page: number;
|
||||
onChangePage: (page: number) => void;
|
||||
total: number;
|
||||
pageSize: number;
|
||||
onChangePageSize: (pageSize: number) => void;
|
||||
};
|
||||
export const CITable: FC<Props> = ({
|
||||
projectName,
|
||||
loading,
|
||||
pageSize,
|
||||
page,
|
||||
retry,
|
||||
builds,
|
||||
onChangePage,
|
||||
onChangePageSize,
|
||||
total,
|
||||
}) => {
|
||||
return (
|
||||
<Table
|
||||
options={{ paging: false }}
|
||||
isLoading={loading}
|
||||
options={{ paging: true, pageSize }}
|
||||
totalCount={total}
|
||||
page={page}
|
||||
actions={[
|
||||
{
|
||||
icon: () => <RetryIcon />,
|
||||
tooltip: 'Refresh Data',
|
||||
isFreeAction: true,
|
||||
onClick: () => retry(),
|
||||
},
|
||||
]}
|
||||
data={builds}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangePageSize}
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<GitHubIcon />
|
||||
@@ -132,4 +164,4 @@ export const CITable: FC<{
|
||||
columns={generatedColumns}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
List,
|
||||
Grid,
|
||||
ListItem,
|
||||
Snackbar,
|
||||
Box,
|
||||
} from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { InfoCard, Content } from '@backstage/core';
|
||||
import { Layout } from '../../components/Layout';
|
||||
import { PluginHeader } from '../../components/PluginHeader';
|
||||
import { useSettings } from '../../state/useSettings';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const [
|
||||
{ repo: repoFromStore, owner: ownerFromStore, token: tokenFromStore },
|
||||
{ saveSettings },
|
||||
] = useSettings();
|
||||
|
||||
const [token, setToken] = React.useState(() => tokenFromStore);
|
||||
const [owner, setOwner] = React.useState(() => ownerFromStore);
|
||||
const [repo, setRepo] = React.useState(() => repoFromStore);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tokenFromStore !== token) {
|
||||
setToken(tokenFromStore);
|
||||
}
|
||||
if (ownerFromStore !== owner) {
|
||||
setOwner(ownerFromStore);
|
||||
}
|
||||
if (repoFromStore !== repo) {
|
||||
setRepo(repoFromStore);
|
||||
}
|
||||
}, [ownerFromStore, repoFromStore, tokenFromStore]);
|
||||
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Content>
|
||||
<PluginHeader title="Settings" />
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={3}>
|
||||
<InfoCard
|
||||
title={
|
||||
<>
|
||||
Project Credentials
|
||||
{/* {authed ? <StatusOK /> : <StatusFailed />} */}
|
||||
<Snackbar
|
||||
autoHideDuration={1000}
|
||||
open={saved}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
onClose={() => setSaved(false)}
|
||||
>
|
||||
<Alert severity="success">Credentials saved.</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<List>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-token"
|
||||
label="Token"
|
||||
value={token}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-owner"
|
||||
fullWidth
|
||||
label="Owner"
|
||||
variant="outlined"
|
||||
value={owner}
|
||||
onChange={(e) => setOwner(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-repo"
|
||||
label="Repo"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={repo}
|
||||
onChange={(e) => setRepo(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Box
|
||||
mt={2}
|
||||
display="flex"
|
||||
width="100%"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Button
|
||||
data-testid="github-auth-button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setSaved(true);
|
||||
saveSettings({ repo, owner, token });
|
||||
}}
|
||||
>
|
||||
Save credentials
|
||||
</Button>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
import React, { FC, useReducer, Dispatch, Reducer } from 'react';
|
||||
import { circleCIApiRef } from '../api';
|
||||
import type { SettingsState, State, Action } from './types';
|
||||
export type { SettingsState };
|
||||
import equal from 'fast-deep-equal';
|
||||
import type { State, Action, SettingsState } from './types';
|
||||
export { SettingsState };
|
||||
|
||||
export const AppContext = React.createContext<[State, Dispatch<Action>]>(
|
||||
[] as any,
|
||||
@@ -25,13 +24,10 @@ export const AppContext = React.createContext<[State, Dispatch<Action>]>(
|
||||
export const STORAGE_KEY = `${circleCIApiRef.id}.settings`;
|
||||
|
||||
const initialState: State = {
|
||||
settings: {
|
||||
owner: '',
|
||||
repo: '',
|
||||
token: '',
|
||||
},
|
||||
builds: [],
|
||||
buildsWithSteps: {},
|
||||
owner: '',
|
||||
repo: '',
|
||||
token: '',
|
||||
showSettings: false,
|
||||
};
|
||||
|
||||
const reducer: Reducer<State, Action> = (state, action) => {
|
||||
@@ -39,23 +35,12 @@ const reducer: Reducer<State, Action> = (state, action) => {
|
||||
case 'setCredentials':
|
||||
return {
|
||||
...state,
|
||||
settings: { ...state.settings, ...action.payload },
|
||||
...action.payload,
|
||||
};
|
||||
case 'setBuilds':
|
||||
if (equal(action.payload, state.builds)) return state;
|
||||
return {
|
||||
...state,
|
||||
builds: action.payload,
|
||||
};
|
||||
case 'setBuildWithSteps': {
|
||||
return {
|
||||
...state,
|
||||
buildsWithSteps: {
|
||||
...state.buildsWithSteps,
|
||||
[action.payload.build_num!]: action.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'showSettings':
|
||||
return { ...state, showSettings: true };
|
||||
case 'hideSettings':
|
||||
return { ...state, showSettings: false };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -63,7 +48,6 @@ const reducer: Reducer<State, Action> = (state, action) => {
|
||||
|
||||
export const AppStateProvider: FC = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={[state, dispatch]}>
|
||||
<>{children}</>
|
||||
|
||||
@@ -14,3 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './AppState';
|
||||
export * from './useSettings';
|
||||
export * from './useBuilds';
|
||||
export * from './useBuildWithSteps';
|
||||
|
||||
@@ -13,41 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { BuildSummary, BuildWithSteps } from '../api';
|
||||
|
||||
export type SettingsState = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
token: string;
|
||||
export type Settings = { owner: string; repo: string; token: string };
|
||||
export type SettingsState = Settings & {
|
||||
showSettings: boolean;
|
||||
};
|
||||
|
||||
export type BuildsState = BuildSummary[];
|
||||
export type State = SettingsState;
|
||||
|
||||
export type State = {
|
||||
settings: SettingsState;
|
||||
builds: BuildsState;
|
||||
buildsWithSteps: BuildsWithStepsState;
|
||||
};
|
||||
type SettingsAction =
|
||||
| {
|
||||
type: 'setCredentials';
|
||||
payload: {
|
||||
repo: string;
|
||||
owner: string;
|
||||
token: string;
|
||||
};
|
||||
}
|
||||
| { type: 'showSettings' }
|
||||
| { type: 'hideSettings' };
|
||||
|
||||
type SettingsAction = {
|
||||
type: 'setCredentials';
|
||||
payload: {
|
||||
repo: string;
|
||||
owner: string;
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
type BuildsAction = {
|
||||
type: 'setBuilds';
|
||||
payload: BuildSummary[];
|
||||
};
|
||||
|
||||
type BuildsWithStepsAction = {
|
||||
type: 'setBuildWithSteps';
|
||||
payload: BuildWithSteps;
|
||||
};
|
||||
|
||||
export type BuildsWithStepsState = Record<number, BuildWithSteps>;
|
||||
|
||||
export type Action = SettingsAction | BuildsAction | BuildsWithStepsAction;
|
||||
export type Action = SettingsAction;
|
||||
|
||||
@@ -14,50 +14,47 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { errorApiRef, useApi } from '@backstage/core';
|
||||
import { useContext } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { circleCIApiRef, GitType } from '../api/index';
|
||||
import { AppContext } from '.';
|
||||
import { useAsyncPolling } from './useAsyncPolling';
|
||||
import { useSettings } from './useSettings';
|
||||
|
||||
import { useAsyncPolling } from './useAsyncPolling';
|
||||
|
||||
const INTERVAL_AMOUNT = 3000;
|
||||
|
||||
const INTERVAL_AMOUNT = 1500;
|
||||
export function useBuildWithSteps(buildId: number) {
|
||||
const [settings] = useSettings();
|
||||
const [{ buildsWithSteps }, dispatch] = useContext(AppContext);
|
||||
const [{ token, repo, owner }] = useSettings();
|
||||
const api = useApi(circleCIApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const getBuildWithSteps = async () => {
|
||||
const getBuildWithSteps = useCallback(async () => {
|
||||
if (owner === '' || repo === '' || token === '') {
|
||||
return Promise.reject('No credentials provided');
|
||||
}
|
||||
|
||||
try {
|
||||
const options = {
|
||||
token: settings.token,
|
||||
token: token,
|
||||
vcs: {
|
||||
owner: settings.owner,
|
||||
repo: settings.repo,
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
};
|
||||
const build = await api.getBuild(buildId, options);
|
||||
dispatch({ type: 'setBuildWithSteps', payload: build });
|
||||
return Promise.resolve(build);
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
const { startPolling, stopPolling } = useAsyncPolling(
|
||||
getBuildWithSteps,
|
||||
INTERVAL_AMOUNT,
|
||||
);
|
||||
}, [token, owner, repo, buildId]);
|
||||
|
||||
const restartBuild = async () => {
|
||||
try {
|
||||
await api.retry(buildId, {
|
||||
token: settings.token,
|
||||
token: token,
|
||||
vcs: {
|
||||
owner: settings.owner,
|
||||
repo: settings.repo,
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
});
|
||||
@@ -66,15 +63,22 @@ export function useBuildWithSteps(buildId: number) {
|
||||
}
|
||||
};
|
||||
|
||||
const build = buildsWithSteps[buildId];
|
||||
const { loading, value, retry } = useAsyncRetry(() => getBuildWithSteps(), [
|
||||
getBuildWithSteps,
|
||||
]);
|
||||
|
||||
const { startPolling, stopPolling } = useAsyncPolling(
|
||||
getBuildWithSteps,
|
||||
INTERVAL_AMOUNT,
|
||||
);
|
||||
|
||||
return [
|
||||
build,
|
||||
{ loading, value, retry },
|
||||
{
|
||||
restartBuild,
|
||||
getBuildWithSteps,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
getBuildWithSteps,
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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 { errorApiRef, useApi } from '@backstage/core';
|
||||
import { BuildSummary, GitType } from 'circleci-api';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { circleCIApiRef } from '../api/index';
|
||||
import { CITableBuildInfo } from '../pages/BuildsPage/lib/CITable';
|
||||
import { useSettings } from './useSettings';
|
||||
|
||||
const makeReadableStatus = (status: string | undefined) => {
|
||||
if (!status) return '';
|
||||
return ({
|
||||
retried: 'Retried',
|
||||
canceled: 'Canceled',
|
||||
infrastructure_fail: 'Infra fail',
|
||||
timedout: 'Timedout',
|
||||
not_run: 'Not run',
|
||||
running: 'Running',
|
||||
failed: 'Failed',
|
||||
queued: 'Queued',
|
||||
scheduled: 'Scheduled',
|
||||
not_running: 'Not running',
|
||||
no_tests: 'No tests',
|
||||
fixed: 'Fixed',
|
||||
success: 'Success',
|
||||
} as Record<string, string>)[status];
|
||||
};
|
||||
|
||||
export const transform = (
|
||||
buildsData: BuildSummary[],
|
||||
restartBuild: { (buildId: number): Promise<void> },
|
||||
): CITableBuildInfo[] => {
|
||||
return buildsData.map((buildData) => {
|
||||
const tableBuildInfo: CITableBuildInfo = {
|
||||
id: String(buildData.build_num),
|
||||
buildName: buildData.subject
|
||||
? buildData.subject +
|
||||
(buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '')
|
||||
: '',
|
||||
onRestartClick: () =>
|
||||
typeof buildData.build_num !== 'undefined' &&
|
||||
restartBuild(buildData.build_num),
|
||||
source: {
|
||||
branchName: String(buildData.branch),
|
||||
commit: {
|
||||
hash: String(buildData.vcs_revision),
|
||||
url: 'todo',
|
||||
},
|
||||
},
|
||||
status: makeReadableStatus(buildData.status),
|
||||
buildUrl: buildData.build_url,
|
||||
};
|
||||
return tableBuildInfo;
|
||||
});
|
||||
};
|
||||
|
||||
export function useBuilds() {
|
||||
const [{ repo, owner, token }] = useSettings();
|
||||
|
||||
const api = useApi(circleCIApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
|
||||
const getBuilds = useCallback(
|
||||
async ({ limit, offset }: { limit: number; offset: number }) => {
|
||||
if (owner === '' || repo === '' || token === '') {
|
||||
return Promise.reject('No credentials provided');
|
||||
}
|
||||
|
||||
try {
|
||||
return await api.getBuilds(
|
||||
{ limit, offset },
|
||||
{
|
||||
token: token,
|
||||
vcs: {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[repo, token, owner],
|
||||
);
|
||||
|
||||
const restartBuild = async (buildId: number) => {
|
||||
try {
|
||||
await api.retry(buildId, {
|
||||
token: token,
|
||||
vcs: {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getBuilds({ limit: 1, offset: 0 }).then((b) => setTotal(b?.[0].build_num!));
|
||||
}, [repo]);
|
||||
|
||||
const { loading, value, retry } = useAsyncRetry(
|
||||
() =>
|
||||
getBuilds({
|
||||
offset: page * pageSize,
|
||||
limit: pageSize,
|
||||
}).then((builds) => transform(builds ?? [], restartBuild)),
|
||||
[page, pageSize, getBuilds],
|
||||
);
|
||||
|
||||
const projectName = `${owner}/${repo}`;
|
||||
return [
|
||||
{
|
||||
page,
|
||||
pageSize,
|
||||
loading,
|
||||
value,
|
||||
projectName,
|
||||
total,
|
||||
},
|
||||
{
|
||||
getBuilds,
|
||||
setPage,
|
||||
setPageSize,
|
||||
restartBuild,
|
||||
retry,
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* 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 { errorApiRef, useApi } from '@backstage/core';
|
||||
import { GitType } from 'circleci-api';
|
||||
import { useContext } from 'react';
|
||||
import { circleCIApiRef } from '../api/index';
|
||||
import { AppContext } from '.';
|
||||
import { useAsyncPolling } from './useAsyncPolling';
|
||||
|
||||
const INTERVAL_AMOUNT = 3000;
|
||||
|
||||
export function useBuilds() {
|
||||
const [{ builds, settings }, dispatch] = useContext(AppContext);
|
||||
|
||||
const api = useApi(circleCIApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const getBuilds = async () => {
|
||||
if (settings.owner === '' || settings.repo === '') return;
|
||||
try {
|
||||
const newBuilds = await api.getBuilds({
|
||||
token: settings.token,
|
||||
vcs: {
|
||||
owner: settings.owner,
|
||||
repo: settings.repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: 'setBuilds',
|
||||
payload: newBuilds,
|
||||
});
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
};
|
||||
|
||||
const { startPolling, stopPolling } = useAsyncPolling(
|
||||
getBuilds,
|
||||
INTERVAL_AMOUNT,
|
||||
);
|
||||
|
||||
const restartBuild = async (buildId: number) => {
|
||||
try {
|
||||
await api.retry(buildId, {
|
||||
token: settings.token,
|
||||
vcs: {
|
||||
owner: settings.owner,
|
||||
repo: settings.repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
builds,
|
||||
{
|
||||
restartBuild,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
@@ -13,12 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { errorApiRef, useApi } from '@backstage/core';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { AppContext, STORAGE_KEY, SettingsState } from '.';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AppContext, STORAGE_KEY } from './AppState';
|
||||
import { Settings } from './types';
|
||||
|
||||
export function useSettings() {
|
||||
const [{ settings }, dispatch] = useContext(AppContext);
|
||||
const [settings, dispatch] = useContext(AppContext);
|
||||
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
@@ -44,20 +45,22 @@ export function useSettings() {
|
||||
rehydrate();
|
||||
}, []);
|
||||
|
||||
const persist = (state: SettingsState) => {
|
||||
const persist = (state: Settings) => {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
};
|
||||
|
||||
return [
|
||||
settings,
|
||||
{
|
||||
saveSettings: (state: SettingsState) => {
|
||||
saveSettings: (state: Settings) => {
|
||||
persist(state);
|
||||
dispatch({
|
||||
type: 'setCredentials',
|
||||
payload: state,
|
||||
});
|
||||
},
|
||||
showSettings: () => dispatch({ type: 'showSettings' }),
|
||||
hideSettings: () => dispatch({ type: 'hideSettings' }),
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
|
||||
@@ -18376,10 +18376,10 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
|
||||
source-map-url "^0.4.0"
|
||||
urix "^0.1.0"
|
||||
|
||||
source-map-support@^0.5.6, source-map-support@~0.5.12:
|
||||
version "0.5.16"
|
||||
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
|
||||
integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
|
||||
source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12:
|
||||
version "0.5.19"
|
||||
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
||||
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
source-map "^0.6.0"
|
||||
@@ -19659,14 +19659,14 @@ ts-loader@^7.0.4:
|
||||
semver "^6.0.0"
|
||||
|
||||
ts-node@^8.6.2:
|
||||
version "8.8.1"
|
||||
resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.8.1.tgz#7c4d3e9ed33aa703b64b28d7f9d194768be5064d"
|
||||
integrity sha512-10DE9ONho06QORKAaCBpPiFCdW+tZJuY/84tyypGtl6r+/C7Asq0dhqbRZURuUlLQtZxxDvT8eoj8cGW0ha6Bg==
|
||||
version "8.10.1"
|
||||
resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.1.tgz#77da0366ff8afbe733596361d2df9a60fc9c9bd3"
|
||||
integrity sha512-bdNz1L4ekHiJul6SHtZWs1ujEKERJnHs4HxN7rjTyyVOFf3HaJ6sLqe6aPG62XTzAB/63pKRh5jTSWL0D7bsvw==
|
||||
dependencies:
|
||||
arg "^4.1.0"
|
||||
diff "^4.0.1"
|
||||
make-error "^1.1.1"
|
||||
source-map-support "^0.5.6"
|
||||
source-map-support "^0.5.17"
|
||||
yn "3.1.1"
|
||||
|
||||
ts-pnp@^1.1.2:
|
||||
|
||||
Reference in New Issue
Block a user