Skip to content

Commit

Permalink
Added ability to select multiple apps (#146)
Browse files Browse the repository at this point in the history
* Added ability to select multiple apps

* Added API calls

* clean up

* remove the redundant delete

* Fixed redundant volumes
  • Loading branch information
githubsaturn authored Apr 28, 2024
1 parent 71c211b commit a4cfc51
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 149 deletions.
7 changes: 6 additions & 1 deletion src/api/ApiManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,14 +330,19 @@ export default class ApiManager {
)
}

deleteApp(appName: string, volumes: string[]) {
deleteApp(
appName: string | undefined,
volumes: string[],
appNames: string[] | undefined
) {
const http = this.http

return Promise.resolve() //
.then(
http.fetch(http.POST, '/user/apps/appDefinitions/delete', {
appName,
volumes,
appNames,
})
)
}
Expand Down
4 changes: 4 additions & 0 deletions src/containers/apps/Apps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,12 @@ export default class Apps extends ApiComponent<
<Row justify="center">
<Col xs={{ span: 24 }} lg={{ span: 20 }}>
<AppsTable
onReloadRequested={() => {
self.reFetchData()
}}
search={self.props.location.search}
history={self.props.history}
apiManager={self.apiManager}
defaultNginxConfig={apiData.defaultNginxConfig}
apps={apiData.appDefinitions}
rootDomain={apiData.rootDomain}
Expand Down
98 changes: 90 additions & 8 deletions src/containers/apps/AppsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,54 @@
import {
CheckOutlined,
CodeOutlined,
DeleteOutlined,
DisconnectOutlined,
LinkOutlined,
LoadingOutlined,
MenuOutlined,
} from '@ant-design/icons'
import { Card, Input, Row, Table, Tag, Tooltip } from 'antd'
import { Button, Card, Input, Row, Table, Tag, Tooltip } from 'antd'
import { ColumnProps } from 'antd/lib/table'
import { History } from 'history'
import { Component, Fragment } from 'react'
import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import ApiManager from '../../api/ApiManager'
import { IMobileComponent } from '../../models/ContainerProps'
import { localize } from '../../utils/Language'
import Logger from '../../utils/Logger'
import NewTabLink from '../global/NewTabLink'
import Timestamp from '../global/Timestamp'
import { IAppDef } from './AppDefinition'
import onDeleteAppClicked from './DeleteAppConfirm'

type TableData = IAppDef & { lastDeployTime: string }

class AppsTable extends Component<
{
history: History
apps: IAppDef[]
apiManager: ApiManager
onReloadRequested: () => void
rootDomain: string
defaultNginxConfig: string
isMobile: boolean
search: string | undefined
},
{ searchTerm: string }
{
searchTerm: string
isBulkEditMode: boolean
selectedRowKeys: React.Key[]
}
> {
constructor(props: any) {
super(props)
const urlsQuery = new URLSearchParams(props.search || '').get('q') || ''
this.state = { searchTerm: urlsQuery }
this.state = {
searchTerm: urlsQuery,
isBulkEditMode: false,
selectedRowKeys: [],
}
}

appDetailPath(appName: string) {
Expand Down Expand Up @@ -267,7 +281,53 @@ class AppsTable extends Component<

return (
<Card
extra={!self.props.isMobile && searchAppInput}
extra={
<div>
<Button
type="text"
onClick={() => {
const newState = !self.state.isBulkEditMode
self.setState({
isBulkEditMode: newState,
})
if (!newState)
self.setState({ selectedRowKeys: [] })
}}
>
<MenuOutlined />
</Button>
{self.state.isBulkEditMode && (
<div>
<Button
disabled={
!self.state.selectedRowKeys ||
self.state.selectedRowKeys.length === 0
}
type="text"
danger={true}
onClick={() => {
onDeleteAppClicked(
self.props.apps.filter(
(a) =>
a.appName &&
self.state.selectedRowKeys.includes(
a.appName
)
),
self.props.apiManager,
(success) => {
// with or without errors, let's refresh the page
self.props.onReloadRequested()
}
)
}}
>
<DeleteOutlined />
</Button>
</div>
)}
</div>
}
title={
<div
style={{
Expand All @@ -276,9 +336,15 @@ class AppsTable extends Component<
}}
>
<div>
<CodeOutlined />
&nbsp;&nbsp;&nbsp;
{localize('apps_table.title', 'Your Apps')}
<div style={{ maxWidth: 250 }}>
<CodeOutlined />
<span
style={{ marginRight: 20, marginLeft: 5 }}
>
{localize('apps_table.title', 'Your Apps')}
</span>
{!self.props.isMobile && searchAppInput}
</div>
</div>

{self.props.isMobile && (
Expand Down Expand Up @@ -359,6 +425,22 @@ class AppsTable extends Component<
dataSource={appsToRender}
pagination={false}
size="middle"
rowSelection={
self.state.isBulkEditMode
? {
selectedRowKeys:
self.state.selectedRowKeys,
onChange: (
newSelectedRowKeys: React.Key[]
) => {
self.setState({
selectedRowKeys:
newSelectedRowKeys,
})
},
}
: undefined
}
onChange={(pagination, filters, sorter) => {
// Persist sorter state
if (!Array.isArray(sorter)) {
Expand Down
157 changes: 157 additions & 0 deletions src/containers/apps/DeleteAppConfirm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Checkbox, Input, Modal, message } from 'antd'
import ApiManager from '../../api/ApiManager'
import { IHashMapGeneric } from '../../models/IHashMapGeneric'
import Toaster from '../../utils/Toaster'
import Utils from '../../utils/Utils'
import NewTabLink from '../global/NewTabLink'
import { IAppDef } from './AppDefinition'

export default function onDeleteAppClicked(
appDefinitionsInput: IAppDef[],
apiManager: ApiManager,
onFinished: (success: boolean) => void
) {
const volumesToDelete: IHashMapGeneric<boolean> = {}
const allVolumesNames = new Set<string>()

const appDefinitions: IAppDef[] = []

const confirmation = { confirmationText: '' }

appDefinitionsInput.forEach((a) => {
appDefinitions.push(Utils.copyObject(a))
})

appDefinitions.forEach((appDef) => {
if (appDef.volumes) {
appDef.volumes.forEach((v) => {
if (v.volumeName) {
allVolumesNames.add(v.volumeName)
volumesToDelete[v.volumeName] = true
}
})
}
})

const appsList = (
<ul>
{appDefinitions.map((a) => (
<li key={a.appName || ''}>
{' '}
<code>{a.appName}</code>
</li>
))}
</ul>
)

Modal.confirm({
okType: 'danger',
title: 'Confirm Permanent Delete?',
content: (
<div>
<div>
You are about to delete {appsList}
Please note that this is
<b> not reversible</b>.
</div>
<p className={allVolumesNames.size ? '' : 'hide-on-demand'}>
Please select the volumes you want to delete. Note that if
any of the volumes are being used by other CapRover apps,
they will not be deleted even if you select them. Deleting
volumes takes <b>more than 10 seconds</b>, please be patient
</p>
{Array.from(allVolumesNames).map((v) => {
return (
<div key={v}>
<Checkbox
defaultChecked={!!volumesToDelete[v]}
onChange={(e: any) => {
volumesToDelete[v] = !volumesToDelete[v]
}}
>
{v}
</Checkbox>
</div>
)
})}
<p style={{ marginTop: 25 }}>
Type CONFIRM in the box below to confirm deletion of this
app:
</p>
<Input
type="text"
placeholder={'CONFIRM'}
onChange={(e) => {
confirmation.confirmationText = e.target.value.trim()
}}
/>
</div>
),
onOk() {
if (confirmation.confirmationText.toLowerCase() !== 'confirm') {
message.warning(
'Confirm text did not match. Operation cancelled.'
)
return
}
const volumes: string[] = []
Object.keys(volumesToDelete).forEach((v) => {
if (volumesToDelete[v]) {
volumes.push(v)
}
})

return apiManager
.deleteApp(
undefined,
volumes,
appDefinitions.map((a) => a.appName || '')
)
.then(function (data) {
const volumesFailedToDelete =
data.volumesFailedToDelete as string[]
if (volumesFailedToDelete && volumesFailedToDelete.length) {
Modal.info({
title: "Some volumes weren't deleted!",
content: (
<div>
<p>
Some volumes weren't deleted because
they were probably being used by other
containers. Sometimes, this is because
of a temporary delay when the original
container deletion was done with a
delay. Please consult the{' '}
<NewTabLink url="https://caprover.com/docs/persistent-apps.html#removing-persistent-apps">
documentation
</NewTabLink>{' '}
and delete them manually if needed.
Skipped volumes are:
</p>
<ul>
{volumesFailedToDelete.map((v) => (
<li>
<code>{v}</code>
</li>
))}
</ul>
</div>
),
})
}
message.success('App deleted!')
})
.then(function () {
onFinished(true)
})
.catch(
Toaster.createCatcher(function () {
onFinished(false)
})
)
},
onCancel() {
// do nothing
},
})
}
Loading

0 comments on commit a4cfc51

Please sign in to comment.