Allow User to Cleanup Repository from UI (#53047)

* Added repository cleanup button. Added logic for spinner while loading, added new repository request, type and telemetry metric.

* Added additional bindings for server side to hit the cleanup endpoint.

* fix cleanup request

* Added data test subject to the code editors to differentiate them and fixed a broken inport of RepositoryCleanup.

* Added files for a component integration test. The tests are failing right now so we need to get those green. Added a functional test. Need to set up kbn-es to be able to set up a file repository before being able to run the functional tests.

* Added change to the way data-test-subjects were created for the repository list table so that columns can be individually identified. Added functional test to allow checking the details of repositories.

* Removed the jest tests for repository details until we get jest fixed.

* Fixed jest test to reflect updated test subjects.

* Made changes per feedback in PR comments.

* Fixed i10n issues using <FormattedMessage>. Removed reference to blueBird and used Promise.all(). Fixed all nits in PR comments.

* Added i10n fixes for header.

* Added i10n fixes for header.

* Added name parameter for i18n strings.

* Removed i18n string from JSON.stringify call since it's already a string.

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Alison Goryachev <alisonmllr20@gmail.com>
This commit is contained in:
John Dorlus 2020-01-11 02:51:35 -05:00 committed by GitHub
parent 51e51ca434
commit 10733b5415
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 295 additions and 60 deletions

View file

@ -99,7 +99,7 @@ export const setup = async (): Promise<HomeTestBed> => {
const tabs = ['snapshots', 'repositories'];
testBed
.find('tab')
.find(`${tab}_tab`)
.at(tabs.indexOf(tab))
.simulate('click');
};
@ -360,7 +360,10 @@ export type TestSubjects =
| 'state'
| 'state.title'
| 'state.value'
| 'tab'
| 'repositories_tab'
| 'snapshots_tab'
| 'policies_tab'
| 'restore_status_tab'
| 'tableHeaderCell_durationInMillis_3'
| 'tableHeaderCell_durationInMillis_3.tableHeaderSortButton'
| 'tableHeaderCell_indices_4'

View file

@ -95,6 +95,16 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
const setCleanupRepositoryResponse = (response?: HttpResponse, error?: any) => {
const status = error ? error.status || 503 : 200;
server.respondWith('POST', `${API_BASE_PATH}repositories/:name/cleanup`, [
status,
{ 'Content-Type': 'application/json' },
JSON.stringify(response),
]);
};
const setGetPolicyResponse = (response?: HttpResponse) => {
server.respondWith('GET', `${API_BASE_PATH}policy/:name`, [
200,
@ -113,6 +123,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
setLoadIndicesResponse,
setAddPolicyResponse,
setGetPolicyResponse,
setCleanupRepositoryResponse,
};
};

View file

@ -88,8 +88,15 @@ describe('<SnapshotRestoreHome />', () => {
test('should have 4 tabs', () => {
const { find } = testBed;
expect(find('tab').length).toBe(4);
expect(find('tab').map(t => t.text())).toEqual([
const tabs = [
find('snapshots_tab'),
find('repositories_tab'),
find('policies_tab'),
find('restore_status_tab'),
];
expect(tabs.length).toBe(4);
expect(tabs.map(t => t.text())).toEqual([
'Snapshots',
'Repositories',
'Policies',

View file

@ -157,3 +157,15 @@ export interface InvalidRepositoryVerification {
}
export type RepositoryVerification = ValidRepositoryVerification | InvalidRepositoryVerification;
export interface SuccessfulRepositoryCleanup {
cleaned: true;
response: object;
}
export interface FailedRepositoryCleanup {
cleaned: false;
error: object;
}
export type RepositoryCleanup = FailedRepositoryCleanup | SuccessfulRepositoryCleanup;

View file

@ -103,6 +103,7 @@ export const UIM_REPOSITORY_DELETE = 'repository_delete';
export const UIM_REPOSITORY_DELETE_MANY = 'repository_delete_many';
export const UIM_REPOSITORY_SHOW_DETAILS_CLICK = 'repository_show_details_click';
export const UIM_REPOSITORY_DETAIL_PANEL_VERIFY = 'repository_detail_panel_verify';
export const UIM_REPOSITORY_DETAIL_PANEL_CLEANUP = 'repository_detail_panel_cleanup';
export const UIM_SNAPSHOT_LIST_LOAD = 'snapshot_list_load';
export const UIM_SNAPSHOT_SHOW_DETAILS_CLICK = 'snapshot_show_details_click';
export const UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB = 'snapshot_detail_panel_summary_tab';

View file

@ -150,7 +150,7 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
onClick={() => onSectionChange(tab.id)}
isSelected={tab.id === section}
key={tab.id}
data-test-subj="tab"
data-test-subj={tab.id.toLowerCase() + '_tab'}
>
{tab.name}
</EuiTab>

View file

@ -8,7 +8,6 @@ import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiCodeEditor,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
@ -19,6 +18,8 @@ import {
EuiLink,
EuiSpacer,
EuiTitle,
EuiCodeBlock,
EuiText,
} from '@elastic/eui';
import 'brace/theme/textmate';
@ -28,12 +29,17 @@ import { documentationLinksService } from '../../../../services/documentation';
import {
useLoadRepository,
verifyRepository as verifyRepositoryRequest,
cleanupRepository as cleanupRepositoryRequest,
} from '../../../../services/http';
import { textService } from '../../../../services/text';
import { linkToSnapshots, linkToEditRepository } from '../../../../services/navigation';
import { REPOSITORY_TYPES } from '../../../../../../common/constants';
import { Repository, RepositoryVerification } from '../../../../../../common/types';
import {
Repository,
RepositoryVerification,
RepositoryCleanup,
} from '../../../../../../common/types';
import {
RepositoryDeleteProvider,
SectionError,
@ -61,7 +67,9 @@ export const RepositoryDetails: React.FunctionComponent<Props> = ({
const { FormattedMessage } = i18n;
const { error, data: repositoryDetails } = useLoadRepository(repositoryName);
const [verification, setVerification] = useState<RepositoryVerification | undefined>(undefined);
const [cleanup, setCleanup] = useState<RepositoryCleanup | undefined>(undefined);
const [isLoadingVerification, setIsLoadingVerification] = useState<boolean>(false);
const [isLoadingCleanup, setIsLoadingCleanup] = useState<boolean>(false);
const verifyRepository = async () => {
setIsLoadingVerification(true);
@ -70,11 +78,20 @@ export const RepositoryDetails: React.FunctionComponent<Props> = ({
setIsLoadingVerification(false);
};
// Reset verification state when repository name changes, either from adjust URL or clicking
const cleanupRepository = async () => {
setIsLoadingCleanup(true);
const { data } = await cleanupRepositoryRequest(repositoryName);
setCleanup(data.cleanup);
setIsLoadingCleanup(false);
};
// Reset verification state and cleanup when repository name changes, either from adjust URL or clicking
// into a different repository in table list.
useEffect(() => {
setVerification(undefined);
setIsLoadingVerification(false);
setCleanup(undefined);
setIsLoadingCleanup(false);
}, [repositoryName]);
const renderBody = () => {
@ -231,6 +248,8 @@ export const RepositoryDetails: React.FunctionComponent<Props> = ({
<TypeDetails repository={repository} />
<EuiHorizontalRule />
{renderVerification()}
<EuiHorizontalRule />
{renderCleanup()}
</Fragment>
);
};
@ -260,36 +279,13 @@ export const RepositoryDetails: React.FunctionComponent<Props> = ({
</EuiTitle>
<EuiSpacer size="s" />
{verification ? (
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
isReadOnly
value={JSON.stringify(
<EuiCodeBlock language="json" inline={false} data-test-subj="verificationCodeBlock">
{JSON.stringify(
verification.valid ? verification.response : verification.error,
null,
2
)}
setOptions={{
showLineNumbers: false,
tabSize: 2,
maxLines: Infinity,
}}
editorProps={{
$blockScrolling: Infinity,
}}
showGutter={false}
minLines={6}
aria-label={
<FormattedMessage
id="xpack.snapshotRestore.repositoryDetails.verificationDetails"
defaultMessage="Verification details repository '{name}'"
values={{
name,
}}
/>
}
/>
</EuiCodeBlock>
) : null}
<EuiSpacer size="m" />
<EuiButton onClick={verifyRepository} color="primary" isLoading={isLoadingVerification}>
@ -318,6 +314,78 @@ export const RepositoryDetails: React.FunctionComponent<Props> = ({
</Fragment>
);
const renderCleanup = () => (
<>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.repositoryDetails.cleanupTitle"
defaultMessage="Repository cleanup"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.snapshotRestore.repositoryDetails.cleanupRepositoryMessage"
defaultMessage="You can clean up a repository to delete any unreferenced data from a snapshot. This
may provide storage space savings. Note: If you regularly delete snapshots, this
functionality will likely not be as beneficial and should be used less frequently."
/>
</p>
</EuiText>
{cleanup ? (
<>
<EuiSpacer size="s" />
{cleanup.cleaned ? (
<div>
<EuiTitle size="xs">
<h4>
<FormattedMessage
id="xpack.snapshotRestore.repositoryDetails.cleanupDetailsTitle"
defaultMessage="Details"
/>
</h4>
</EuiTitle>
<EuiCodeBlock language="json" inline={false} data-test-subj="cleanupCodeBlock">
{JSON.stringify(cleanup.response, null, 2)}
</EuiCodeBlock>
</div>
) : (
<EuiCallOut
color="danger"
iconType="alert"
title={i18n.translate('xpack.snapshotRestore.repositoryDetails.cleanupErrorTitle', {
defaultMessage: 'Sorry, there was an error cleaning the repository.',
})}
>
<p>
{cleanup.error
? JSON.stringify(cleanup.error)
: i18n.translate('xpack.snapshotRestore.repositoryDetails.cleanupUnknownError', {
defaultMessage: '503: Unknown error',
})}
</p>
</EuiCallOut>
)}
</>
) : null}
<EuiSpacer size="m" />
<EuiButton
onClick={cleanupRepository}
color="primary"
isLoading={isLoadingCleanup}
data-test-subj="cleanupRepositoryButton"
>
<FormattedMessage
id="xpack.snapshotRestore.repositoryDetails.cleanupButtonLabel"
defaultMessage="Clean up repository"
/>
</EuiButton>
</>
);
const renderFooter = () => {
return (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">

View file

@ -96,6 +96,7 @@ export const RepositoryTable: React.FunctionComponent<Props> = ({
},
},
{
field: 'actions',
name: i18n.translate('xpack.snapshotRestore.repositoryList.table.actionsColumnTitle', {
defaultMessage: 'Actions',
}),
@ -302,8 +303,8 @@ export const RepositoryTable: React.FunctionComponent<Props> = ({
rowProps={() => ({
'data-test-subj': 'row',
})}
cellProps={() => ({
'data-test-subj': 'cell',
cellProps={(item, field) => ({
'data-test-subj': `${field.name}_cell`,
})}
data-test-subj="repositoryTable"
/>

View file

@ -11,6 +11,7 @@ import {
UIM_REPOSITORY_DELETE,
UIM_REPOSITORY_DELETE_MANY,
UIM_REPOSITORY_DETAIL_PANEL_VERIFY,
UIM_REPOSITORY_DETAIL_PANEL_CLEANUP,
} from '../../constants';
import { uiMetricService } from '../ui_metric';
import { httpService } from './http';
@ -44,6 +45,20 @@ export const verifyRepository = async (name: Repository['name']) => {
return result;
};
export const cleanupRepository = async (name: Repository['name']) => {
const result = await sendRequest({
path: httpService.addBasePath(
`${API_BASE_PATH}repositories/${encodeURIComponent(name)}/cleanup`
),
method: 'post',
body: undefined,
});
const { trackUiMetric } = uiMetricService;
trackUiMetric(UIM_REPOSITORY_DETAIL_PANEL_CLEANUP);
return result;
};
export const useLoadRepositoryTypes = () => {
return useRequest({
path: httpService.addBasePath(`${API_BASE_PATH}repository_types`),

View file

@ -7,10 +7,10 @@
export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => {
const ca = components.clientAction.factory;
Client.prototype.slm = components.clientAction.namespaceFactory();
const slm = Client.prototype.slm.prototype;
Client.prototype.sr = components.clientAction.namespaceFactory();
const sr = Client.prototype.sr.prototype;
slm.policies = ca({
sr.policies = ca({
urls: [
{
fmt: '/_slm/policy',
@ -19,7 +19,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'GET',
});
slm.policy = ca({
sr.policy = ca({
urls: [
{
fmt: '/_slm/policy/<%=name%>',
@ -33,7 +33,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'GET',
});
slm.deletePolicy = ca({
sr.deletePolicy = ca({
urls: [
{
fmt: '/_slm/policy/<%=name%>',
@ -47,7 +47,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'DELETE',
});
slm.executePolicy = ca({
sr.executePolicy = ca({
urls: [
{
fmt: '/_slm/policy/<%=name%>/_execute',
@ -61,7 +61,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'PUT',
});
slm.updatePolicy = ca({
sr.updatePolicy = ca({
urls: [
{
fmt: '/_slm/policy/<%=name%>',
@ -75,7 +75,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'PUT',
});
slm.executeRetention = ca({
sr.executeRetention = ca({
urls: [
{
fmt: '/_slm/_execute_retention',
@ -83,4 +83,18 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
],
method: 'POST',
});
sr.cleanupRepository = ca({
urls: [
{
fmt: '/_snapshot/<%=name%>/_cleanup',
req: {
name: {
type: 'string',
},
},
},
],
method: 'POST',
});
};

View file

@ -40,7 +40,7 @@ export const getAllHandler: RouterRouteHandler = async (
// Get policies
const policiesByName: {
[key: string]: SlmPolicyEs;
} = await callWithRequest('slm.policies', {
} = await callWithRequest('sr.policies', {
human: true,
});
@ -62,7 +62,7 @@ export const getOneHandler: RouterRouteHandler = async (
const { name } = req.params;
const policiesByName: {
[key: string]: SlmPolicyEs;
} = await callWithRequest('slm.policy', {
} = await callWithRequest('sr.policy', {
name,
human: true,
});
@ -82,7 +82,7 @@ export const getOneHandler: RouterRouteHandler = async (
export const executeHandler: RouterRouteHandler = async (req, callWithRequest) => {
const { name } = req.params;
const { snapshot_name: snapshotName } = await callWithRequest('slm.executePolicy', {
const { snapshot_name: snapshotName } = await callWithRequest('sr.executePolicy', {
name,
});
return { snapshotName };
@ -98,7 +98,7 @@ export const deleteHandler: RouterRouteHandler = async (req, callWithRequest) =>
await Promise.all(
policyNames.map(name => {
return callWithRequest('slm.deletePolicy', { name })
return callWithRequest('sr.deletePolicy', { name })
.then(() => response.itemsDeleted.push(name))
.catch(e =>
response.errors.push({
@ -122,7 +122,7 @@ export const createHandler: RouterRouteHandler = async (req, callWithRequest) =>
// Check that policy with the same name doesn't already exist
try {
const policyByName = await callWithRequest('slm.policy', { name });
const policyByName = await callWithRequest('sr.policy', { name });
if (policyByName[name]) {
throw conflictError;
}
@ -134,7 +134,7 @@ export const createHandler: RouterRouteHandler = async (req, callWithRequest) =>
}
// Otherwise create new policy
return await callWithRequest('slm.updatePolicy', {
return await callWithRequest('sr.updatePolicy', {
name,
body: serializePolicy(policy),
});
@ -146,10 +146,10 @@ export const updateHandler: RouterRouteHandler = async (req, callWithRequest) =>
// Check that policy with the given name exists
// If it doesn't exist, 404 will be thrown by ES and will be returned
await callWithRequest('slm.policy', { name });
await callWithRequest('sr.policy', { name });
// Otherwise update policy
return await callWithRequest('slm.updatePolicy', {
return await callWithRequest('sr.updatePolicy', {
name,
body: serializePolicy(policy),
});
@ -210,5 +210,5 @@ export const updateRetentionSettingsHandler: RouterRouteHandler = async (req, ca
};
export const executeRetentionHandler: RouterRouteHandler = async (_req, callWithRequest) => {
return await callWithRequest('slm.executeRetention');
return await callWithRequest('sr.executeRetention');
};

View file

@ -15,6 +15,7 @@ import {
RepositoryType,
RepositoryVerification,
SlmPolicyEs,
RepositoryCleanup,
} from '../../../common/types';
import { Plugins } from '../../shim';
@ -34,6 +35,7 @@ export function registerRepositoriesRoutes(router: Router, plugins: Plugins) {
router.get('repositories', getAllHandler);
router.get('repositories/{name}', getOneHandler);
router.get('repositories/{name}/verify', getVerificationHandler);
router.post('repositories/{name}/cleanup', getCleanupHandler);
router.put('repositories', createHandler);
router.put('repositories/{name}', updateHandler);
router.delete('repositories/{names}', deleteHandler);
@ -74,7 +76,7 @@ export const getAllHandler: RouterRouteHandler = async (
try {
const policiesByName: {
[key: string]: SlmPolicyEs;
} = await callWithRequest('slm.policies', {
} = await callWithRequest('sr.policies', {
human: true,
});
const managedRepositoryPolicy = Object.entries(policiesByName)
@ -172,6 +174,31 @@ export const getVerificationHandler: RouterRouteHandler = async (
};
};
export const getCleanupHandler: RouterRouteHandler = async (
req,
callWithRequest
): Promise<{
cleanup: RepositoryCleanup | {};
}> => {
const { name } = req.params;
const cleanupResults = await callWithRequest('sr.cleanupRepository', {
name,
}).catch(e => ({
cleaned: false,
error: e.response ? JSON.parse(e.response) : e,
}));
return {
cleanup: cleanupResults.error
? cleanupResults
: {
cleaned: true,
response: cleanupResults,
},
};
};
export const getTypesHandler: RouterRouteHandler = async () => {
// In ECE/ESS, do not enable the default types
const types: RepositoryType[] = isCloudEnabled ? [] : [...DEFAULT_REPOSITORY_TYPES];

View file

@ -38,7 +38,7 @@ export const getAllHandler: RouterRouteHandler = async (
// Attempt to retrieve policies
// This could fail if user doesn't have access to read SLM policies
try {
const policiesByName = await callWithRequest('slm.policies');
const policiesByName = await callWithRequest('sr.policies');
policies = Object.keys(policiesByName);
} catch (e) {
// Silently swallow error as policy names aren't required in UI

View file

@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n';
import { Legacy } from 'kibana';
import { createRouter, Router } from '../../../server/lib/create_router';
import { registerLicenseChecker } from '../../../server/lib/register_license_checker';
import { elasticsearchJsPlugin } from './client/elasticsearch_slm';
import { elasticsearchJsPlugin } from './client/elasticsearch_sr';
import { CloudSetup } from '../../../../plugins/cloud/server';
export interface Core {
http: {

View file

@ -10965,7 +10965,6 @@
"xpack.snapshotRestore.repositoryDetails.typeS3.serverSideEncryptionLabel": "サーバー側エコシステム",
"xpack.snapshotRestore.repositoryDetails.typeS3.storageClassLabel": "ストレージクラス",
"xpack.snapshotRestore.repositoryDetails.typeTitle": "レポジトリタイプ",
"xpack.snapshotRestore.repositoryDetails.verificationDetails": "認証情報レポジトリ「{name}」",
"xpack.snapshotRestore.repositoryDetails.verificationDetailsTitle": "詳細",
"xpack.snapshotRestore.repositoryDetails.verificationTitle": "認証ステータス",
"xpack.snapshotRestore.repositoryDetails.verifyButtonLabel": "レポジトリを検証",

View file

@ -11054,7 +11054,6 @@
"xpack.snapshotRestore.repositoryDetails.typeS3.serverSideEncryptionLabel": "服务器端加密",
"xpack.snapshotRestore.repositoryDetails.typeS3.storageClassLabel": "存储类",
"xpack.snapshotRestore.repositoryDetails.typeTitle": "存储库类型",
"xpack.snapshotRestore.repositoryDetails.verificationDetails": "验证详情存储库“{name}”",
"xpack.snapshotRestore.repositoryDetails.verificationDetailsTitle": "详情",
"xpack.snapshotRestore.repositoryDetails.verificationTitle": "验证状态",
"xpack.snapshotRestore.repositoryDetails.verifyButtonLabel": "验证存储库",

View file

@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'snapshotRestore']);
const log = getService('log');
const es = getService('legacyEs');
describe('Home page', function() {
this.tags('smoke');
@ -26,5 +27,37 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const repositoriesButton = await pageObjects.snapshotRestore.registerRepositoryButton();
expect(await repositoriesButton.isDisplayed()).to.be(true);
});
describe('Repositories Tab', async () => {
before(async () => {
await es.snapshot.createRepository({
repository: 'my-repository',
body: {
type: 'fs',
settings: {
location: '/tmp/es-backups/',
compress: true,
},
},
verify: true,
});
await pageObjects.snapshotRestore.navToRepositories();
});
it('cleanup repository', async () => {
await pageObjects.snapshotRestore.viewRepositoryDetails('my-repository');
await pageObjects.common.sleep(25000);
const cleanupResponse = await pageObjects.snapshotRestore.performRepositoryCleanup();
await pageObjects.common.sleep(25000);
expect(cleanupResponse).to.contain('results');
expect(cleanupResponse).to.contain('deleted_bytes');
expect(cleanupResponse).to.contain('deleted_blobs');
});
after(async () => {
await es.snapshot.deleteRepository({
repository: 'my-repository',
});
});
});
});
};

View file

@ -69,7 +69,7 @@ export default async function({ readConfigFile }) {
esTestCluster: {
license: 'trial',
from: 'snapshot',
serverArgs: [],
serverArgs: ['path.repo=/tmp/'],
},
kbnTestServer: {

View file

@ -3,11 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrProviderContext } from '../ftr_provider_context';
export function SnapshotRestorePageProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
return {
async appTitleText() {
@ -16,5 +16,50 @@ export function SnapshotRestorePageProvider({ getService }: FtrProviderContext)
async registerRepositoryButton() {
return await testSubjects.find('registerRepositoryButton');
},
async navToRepositories() {
await testSubjects.click('repositories_tab');
await retry.waitForWithTimeout(
'Wait for register repository button to be on page',
10000,
async () => {
return await testSubjects.isDisplayed('registerRepositoryButton');
}
);
},
async getRepoList() {
const table = await testSubjects.find('repositoryTable');
const rows = await table.findAllByCssSelector('[data-test-subj="row"]');
return await Promise.all(
rows.map(async row => {
return {
repoName: await (
await row.findByCssSelector('[data-test-subj="Name_cell"]')
).getVisibleText(),
repoLink: await (
await row.findByCssSelector('[data-test-subj="Name_cell"]')
).findByCssSelector('a'),
repoType: await (
await row.findByCssSelector('[data-test-subj="Type_cell"]')
).getVisibleText(),
repoEdit: await row.findByCssSelector('[data-test-subj="editRepositoryButton"]'),
repoDelete: await row.findByCssSelector('[data-test-subj="deleteRepositoryButton"]'),
};
})
);
},
async viewRepositoryDetails(name: string) {
const repos = await this.getRepoList();
if (repos.length === 1) {
const repoToView = repos.filter(r => (r.repoName = name))[0];
await repoToView.repoLink.click();
}
await retry.waitForWithTimeout(`Repo title should be ${name}`, 10000, async () => {
return (await testSubjects.getVisibleText('title')) === name;
});
},
async performRepositoryCleanup() {
await testSubjects.click('cleanupRepositoryButton');
return await testSubjects.getVisibleText('cleanupCodeBlock');
},
};
}