feat: BILLmanager API на уровне хостера, миграция со старых аккаунтов
Docker / build (push) Failing after 18s
Docker / build (push) Failing after 18s
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* BILLmanager: URL на хостере, учётные данные на аккаунте.
|
||||
*/
|
||||
|
||||
export function providerByIdMap(providers) {
|
||||
return new Map(providers.map((p) => [p.id, p]))
|
||||
}
|
||||
|
||||
/** Аккаунты, для которых можно вызывать синк (есть URL у хостера и креды у аккаунта). */
|
||||
export function billmanagerSyncableAccounts(providerAccounts, providers) {
|
||||
const pmap = providerByIdMap(providers)
|
||||
return providerAccounts.filter((a) => {
|
||||
const p = pmap.get(a.providerId)
|
||||
return p?.apiType === 'billmanager' && (p.apiBaseUrl || '').trim() && a.apiCredentialsSet
|
||||
})
|
||||
}
|
||||
|
||||
/** Показывать кнопки баланса/синка и брать баланс из API. */
|
||||
export function accountBillmanagerUiReady(account, provider) {
|
||||
return (
|
||||
provider?.apiType === 'billmanager' &&
|
||||
(provider.apiBaseUrl || '').trim() &&
|
||||
account.apiCredentialsSet
|
||||
)
|
||||
}
|
||||
|
||||
export function accountUsesBillmanagerBalanceApi(account, provider) {
|
||||
return provider?.apiType === 'billmanager' && account.balance_api != null
|
||||
}
|
||||
@@ -53,13 +53,15 @@ export function lastOkSyncFinishedAt(accountId, syncLog) {
|
||||
* @param {{
|
||||
* vps: object[],
|
||||
* providerAccounts: object[],
|
||||
* providers: object[],
|
||||
* payments: object[],
|
||||
* balanceLedger: object[],
|
||||
* syncLog?: object[],
|
||||
* }} input
|
||||
*/
|
||||
export function computeInventoryHealth(input) {
|
||||
const { vps, providerAccounts, payments, balanceLedger, syncLog = [] } = input
|
||||
const { vps, providerAccounts, providers = [], payments, balanceLedger, syncLog = [] } = input
|
||||
const providerById = new Map(providers.map((p) => [p.id, p]))
|
||||
const now = new Date()
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const ctx = { vps, providerAccounts, payments, balanceLedger, now }
|
||||
@@ -109,9 +111,10 @@ export function computeInventoryHealth(input) {
|
||||
})
|
||||
}
|
||||
|
||||
const bmAccounts = providerAccounts.filter(
|
||||
(a) => a.apiType === 'billmanager' && (a.apiBaseUrl || '').trim() && a.apiCredentialsSet,
|
||||
)
|
||||
const bmAccounts = providerAccounts.filter((a) => {
|
||||
const p = providerById.get(a.providerId)
|
||||
return p?.apiType === 'billmanager' && (p.apiBaseUrl || '').trim() && a.apiCredentialsSet
|
||||
})
|
||||
const staleMs = STALE_SYNC_HOURS * 60 * 60 * 1000
|
||||
const staleAccounts = bmAccounts.filter((a) => {
|
||||
const t = lastOkSyncFinishedAt(a.id, syncLog)
|
||||
@@ -144,12 +147,15 @@ export function computeInventoryHealth(input) {
|
||||
|
||||
/**
|
||||
* @param {object[]} providerAccounts
|
||||
* @param {object[]} providers
|
||||
* @param {object[]} syncLog
|
||||
*/
|
||||
export function getStaleSyncAccountIds(providerAccounts, syncLog, now = new Date()) {
|
||||
const bmAccounts = providerAccounts.filter(
|
||||
(a) => a.apiType === 'billmanager' && (a.apiBaseUrl || '').trim() && a.apiCredentialsSet,
|
||||
)
|
||||
export function getStaleSyncAccountIds(providerAccounts, providers, syncLog, now = new Date()) {
|
||||
const providerById = new Map(providers.map((p) => [p.id, p]))
|
||||
const bmAccounts = providerAccounts.filter((a) => {
|
||||
const p = providerById.get(a.providerId)
|
||||
return p?.apiType === 'billmanager' && (p.apiBaseUrl || '').trim() && a.apiCredentialsSet
|
||||
})
|
||||
const staleMs = STALE_SYNC_HOURS * 60 * 60 * 1000
|
||||
return bmAccounts
|
||||
.filter((a) => {
|
||||
|
||||
+46
-42
@@ -12,6 +12,11 @@ import { ConvertedAmount } from '../components/ConvertedAmount'
|
||||
import { syncAccount, testApiConnection, fetchAccountBalance, fetchSyncStatus } from '../lib/api'
|
||||
import { noBrowserSuggestProps, passwordCredentialInputProps } from '../lib/noBrowserSuggestProps'
|
||||
import { getBalanceMismatchAccountIds, getStaleSyncAccountIds } from '../lib/inventory-health'
|
||||
import {
|
||||
billmanagerSyncableAccounts,
|
||||
accountBillmanagerUiReady,
|
||||
accountUsesBillmanagerBalanceApi,
|
||||
} from '../lib/billmanager-ui'
|
||||
import { IconRefresh, IconPlugConnected } from '@tabler/icons-react'
|
||||
|
||||
const emptyForm = {
|
||||
@@ -21,8 +26,6 @@ const emptyForm = {
|
||||
currency: 'USD',
|
||||
billingMode: 'monthly',
|
||||
notes: '',
|
||||
apiType: '',
|
||||
apiBaseUrl: '',
|
||||
apiLogin: '',
|
||||
apiPassword: '',
|
||||
balance_alert_below: '',
|
||||
@@ -51,8 +54,8 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
}, [])
|
||||
|
||||
const billmanagerAccounts = useMemo(
|
||||
() => db.providerAccounts.filter((a) => a.apiType === 'billmanager' && a.apiBaseUrl),
|
||||
[db.providerAccounts],
|
||||
() => billmanagerSyncableAccounts(db.providerAccounts, db.providers),
|
||||
[db.providerAccounts, db.providers],
|
||||
)
|
||||
const [testConnectionLoading, setTestConnectionLoading] = useState(false)
|
||||
const [testConnectionResult, setTestConnectionResult] = useState(null)
|
||||
@@ -60,13 +63,13 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
|
||||
const highlightAccountIds = useMemo(() => {
|
||||
if (accountsHealth === 'stale-sync') {
|
||||
return new Set(getStaleSyncAccountIds(db.providerAccounts, syncLog))
|
||||
return new Set(getStaleSyncAccountIds(db.providerAccounts, db.providers, syncLog))
|
||||
}
|
||||
if (accountsHealth === 'balance-mismatch') {
|
||||
return new Set(getBalanceMismatchAccountIds(db.providerAccounts, db.balanceLedger))
|
||||
}
|
||||
return null
|
||||
}, [accountsHealth, db.providerAccounts, db.balanceLedger, syncLog])
|
||||
}, [accountsHealth, db.providerAccounts, db.providers, db.balanceLedger, syncLog])
|
||||
|
||||
const balances = useMemo(() => {
|
||||
return db.providerAccounts.map((account) => {
|
||||
@@ -84,7 +87,8 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
const getBalance = (accountId) => balances.find((item) => item.accountId === accountId)?.balance || 0
|
||||
|
||||
const getDisplayBalance = (account) => {
|
||||
if (account.apiType === 'billmanager' && account.balance_api != null) {
|
||||
const provider = db.providers.find((p) => p.id === account.providerId)
|
||||
if (accountUsesBillmanagerBalanceApi(account, provider)) {
|
||||
return account.balance_api
|
||||
}
|
||||
return getBalance(account.id)
|
||||
@@ -116,10 +120,11 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
currency: form.currency,
|
||||
billingMode: form.billingMode,
|
||||
notes: form.notes,
|
||||
apiType: form.apiType || '',
|
||||
apiBaseUrl: form.apiType === 'billmanager' ? form.apiBaseUrl : '',
|
||||
}
|
||||
if (form.apiType === 'billmanager' && form.apiLogin && form.apiPassword) {
|
||||
const selectedProvider = db.providers.find((p) => p.id === form.providerId)
|
||||
const bmReady =
|
||||
selectedProvider?.apiType === 'billmanager' && (selectedProvider.apiBaseUrl || '').trim()
|
||||
if (bmReady && form.apiLogin && form.apiPassword) {
|
||||
payload.apiCredentials = `${form.apiLogin}:${form.apiPassword}`
|
||||
}
|
||||
{
|
||||
@@ -146,14 +151,19 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
}
|
||||
|
||||
const onTestConnection = async () => {
|
||||
if (!form.apiBaseUrl?.trim() || !form.apiLogin?.trim() || !form.apiPassword?.trim()) {
|
||||
setTestConnectionResult({ ok: false, error: 'Заполните URL, логин и пароль' })
|
||||
const p = db.providers.find((x) => x.id === form.providerId)
|
||||
const baseUrl = p?.apiType === 'billmanager' ? (p.apiBaseUrl || '').trim() : ''
|
||||
if (!baseUrl || !form.apiLogin?.trim() || !form.apiPassword?.trim()) {
|
||||
setTestConnectionResult({
|
||||
ok: false,
|
||||
error: 'В настройках хостера укажите URL API; здесь — логин и пароль',
|
||||
})
|
||||
return
|
||||
}
|
||||
setTestConnectionLoading(true)
|
||||
setTestConnectionResult(null)
|
||||
try {
|
||||
const result = await testApiConnection(form.apiBaseUrl, `${form.apiLogin}:${form.apiPassword}`)
|
||||
const result = await testApiConnection(baseUrl, `${form.apiLogin}:${form.apiPassword}`)
|
||||
setTestConnectionResult(result)
|
||||
} catch (err) {
|
||||
setTestConnectionResult({ ok: false, error: err.message || 'Ошибка проверки' })
|
||||
@@ -170,8 +180,6 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
currency: account.currency || 'USD',
|
||||
billingMode: account.billingMode || 'monthly',
|
||||
notes: account.notes || '',
|
||||
apiType: account.apiType || '',
|
||||
apiBaseUrl: account.apiBaseUrl || '',
|
||||
apiLogin: '',
|
||||
apiPassword: '',
|
||||
balance_alert_below:
|
||||
@@ -185,7 +193,12 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
}
|
||||
|
||||
const editingAccount = editingId ? db.providerAccounts.find((a) => a.id === editingId) : null
|
||||
const canTestConnection = form.apiType === 'billmanager' && form.apiBaseUrl?.trim() && form.apiLogin?.trim() && form.apiPassword?.trim()
|
||||
const formProvider = db.providers.find((p) => p.id === form.providerId)
|
||||
const formBmUrl =
|
||||
formProvider?.apiType === 'billmanager' ? (formProvider.apiBaseUrl || '').trim() : ''
|
||||
const canTestConnection = Boolean(
|
||||
formBmUrl && form.apiLogin?.trim() && form.apiPassword?.trim(),
|
||||
)
|
||||
|
||||
const onSync = async (accountId) => {
|
||||
setSyncLoadingId(accountId)
|
||||
@@ -333,6 +346,7 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
<tbody>
|
||||
{db.providerAccounts.map((account) => {
|
||||
const provider = db.providers.find((item) => item.id === account.providerId)
|
||||
const bmUi = accountBillmanagerUiReady(account, provider)
|
||||
const linkedVps = db.vps.filter((item) => item.providerAccountId === account.id)
|
||||
const rowWarn = highlightAccountIds?.has(account.id)
|
||||
return (
|
||||
@@ -374,7 +388,7 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
</td>
|
||||
<td className="text-end">
|
||||
<div className="table-actions d-flex gap-1 flex-wrap justify-content-end">
|
||||
{account.apiType === 'billmanager' && account.apiBaseUrl ? (
|
||||
{bmUi ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
@@ -511,32 +525,22 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<hr className="my-2" />
|
||||
<h6 className="text-secondary mb-2">Интеграция API</h6>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label">Тип API</label>
|
||||
<select autoComplete="off"
|
||||
className="form-select"
|
||||
value={form.apiType}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, apiType: e.target.value, apiBaseUrl: '', apiLogin: '', apiPassword: '' }))}
|
||||
>
|
||||
<option value="">— Не использовать —</option>
|
||||
<option value="billmanager">BILLmanager</option>
|
||||
</select>
|
||||
</div>
|
||||
{form.apiType === 'billmanager' ? (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<label className="form-label">URL API BILLmanager</label>
|
||||
<input {...noBrowserSuggestProps}
|
||||
className="form-control"
|
||||
placeholder="https://bill.example.com:1500/billmgr"
|
||||
value={form.apiBaseUrl}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, apiBaseUrl: e.target.value }))}
|
||||
/>
|
||||
<h6 className="text-secondary mb-2">Учётные данные API BILLmanager</h6>
|
||||
{formProvider?.apiType === 'billmanager' && (formProvider.apiBaseUrl || '').trim() ? (
|
||||
<div className="text-secondary small mb-2">
|
||||
URL: <code className="user-select-all">{formProvider.apiBaseUrl}</code>
|
||||
</div>
|
||||
) : (
|
||||
<div className="alert alert-secondary py-2 small mb-2">
|
||||
В карточке хостера выберите тип API BILLmanager и укажите URL — один на все аккаунты этого
|
||||
хостера.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{formProvider?.apiType === 'billmanager' && (formProvider.apiBaseUrl || '').trim() ? (
|
||||
<>
|
||||
<div className="col-12 col-sm-6">
|
||||
<label className="form-label">Логин</label>
|
||||
<label className="form-label">Логин API</label>
|
||||
<input {...noBrowserSuggestProps}
|
||||
className="form-control"
|
||||
placeholder="admin"
|
||||
@@ -545,7 +549,7 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6">
|
||||
<label className="form-label">Пароль</label>
|
||||
<label className="form-label">Пароль API</label>
|
||||
<input {...passwordCredentialInputProps}
|
||||
type="password"
|
||||
className="form-control"
|
||||
|
||||
@@ -204,11 +204,12 @@ export function DashboardPage({ db = {}, settings, ratesData }) {
|
||||
computeInventoryHealth({
|
||||
vps,
|
||||
providerAccounts,
|
||||
providers,
|
||||
payments,
|
||||
balanceLedger,
|
||||
syncLog: syncLogRows,
|
||||
}),
|
||||
[vps, providerAccounts, payments, balanceLedger, syncLogRows],
|
||||
[vps, providerAccounts, providers, payments, balanceLedger, syncLogRows],
|
||||
)
|
||||
|
||||
const recentSyncFeed = useMemo(() => {
|
||||
|
||||
+137
-2
@@ -3,7 +3,9 @@ import { UiModal } from '../components/UiModal'
|
||||
import { EmptyState } from '../components/EmptyState'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
import { faviconUrlFromWebsite } from '../lib/utils'
|
||||
import { noBrowserSuggestProps } from '../lib/noBrowserSuggestProps'
|
||||
import { noBrowserSuggestProps, passwordCredentialInputProps } from '../lib/noBrowserSuggestProps'
|
||||
import { testApiConnection } from '../lib/api'
|
||||
import { IconPlugConnected } from '@tabler/icons-react'
|
||||
|
||||
const emptyForm = {
|
||||
name: '',
|
||||
@@ -13,12 +15,18 @@ const emptyForm = {
|
||||
usdRate: '',
|
||||
eurRate: '',
|
||||
notes: '',
|
||||
apiType: '',
|
||||
apiBaseUrl: '',
|
||||
}
|
||||
|
||||
export function ProvidersPage({ db, actions }) {
|
||||
const [form, setForm] = useState(emptyForm)
|
||||
const [editingId, setEditingId] = useState(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [apiProbeLogin, setApiProbeLogin] = useState('')
|
||||
const [apiProbePassword, setApiProbePassword] = useState('')
|
||||
const [testConnectionLoading, setTestConnectionLoading] = useState(false)
|
||||
const [testConnectionResult, setTestConnectionResult] = useState(null)
|
||||
|
||||
const onSubmit = (event) => {
|
||||
event.preventDefault()
|
||||
@@ -32,6 +40,9 @@ export function ProvidersPage({ db, actions }) {
|
||||
}
|
||||
setForm(emptyForm)
|
||||
setEditingId(null)
|
||||
setApiProbeLogin('')
|
||||
setApiProbePassword('')
|
||||
setTestConnectionResult(null)
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
@@ -44,11 +55,39 @@ export function ProvidersPage({ db, actions }) {
|
||||
usdRate: provider.usdRate || '',
|
||||
eurRate: provider.eurRate || '',
|
||||
notes: provider.notes || '',
|
||||
apiType: provider.apiType || '',
|
||||
apiBaseUrl: provider.apiBaseUrl || '',
|
||||
})
|
||||
setEditingId(provider.id)
|
||||
setApiProbeLogin('')
|
||||
setApiProbePassword('')
|
||||
setTestConnectionResult(null)
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
const canTestApi =
|
||||
form.apiType === 'billmanager' &&
|
||||
form.apiBaseUrl?.trim() &&
|
||||
apiProbeLogin?.trim() &&
|
||||
apiProbePassword?.trim()
|
||||
|
||||
const onTestConnection = async () => {
|
||||
if (!canTestApi) {
|
||||
setTestConnectionResult({ ok: false, error: 'Заполните URL API и тестовый логин/пароль аккаунта' })
|
||||
return
|
||||
}
|
||||
setTestConnectionLoading(true)
|
||||
setTestConnectionResult(null)
|
||||
try {
|
||||
const result = await testApiConnection(form.apiBaseUrl, `${apiProbeLogin}:${apiProbePassword}`)
|
||||
setTestConnectionResult(result)
|
||||
} catch (err) {
|
||||
setTestConnectionResult({ ok: false, error: err.message || 'Ошибка проверки' })
|
||||
} finally {
|
||||
setTestConnectionLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader pretitle="Справочники" title="Хостеры" />
|
||||
@@ -64,6 +103,9 @@ export function ProvidersPage({ db, actions }) {
|
||||
onClick={() => {
|
||||
setForm(emptyForm)
|
||||
setEditingId(null)
|
||||
setApiProbeLogin('')
|
||||
setApiProbePassword('')
|
||||
setTestConnectionResult(null)
|
||||
setIsModalOpen(true)
|
||||
}}
|
||||
>
|
||||
@@ -79,6 +121,7 @@ export function ProvidersPage({ db, actions }) {
|
||||
<th>Сайт</th>
|
||||
<th>Валюта / курсы</th>
|
||||
<th>Контакт</th>
|
||||
<th>API</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -107,6 +150,13 @@ export function ProvidersPage({ db, actions }) {
|
||||
</div>
|
||||
</td>
|
||||
<td>{provider.contact || '-'}</td>
|
||||
<td>
|
||||
{provider.apiType === 'billmanager' && (provider.apiBaseUrl || '').trim() ? (
|
||||
<span className="text-secondary small">BILLmanager</span>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end">
|
||||
<div className="table-actions">
|
||||
<button
|
||||
@@ -128,7 +178,7 @@ export function ProvidersPage({ db, actions }) {
|
||||
</tr>
|
||||
))}
|
||||
{db.providers.length === 0 ? (
|
||||
<EmptyState message="Пока нет хостеров" colSpan={5} />
|
||||
<EmptyState message="Пока нет хостеров" colSpan={6} />
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -144,6 +194,9 @@ export function ProvidersPage({ db, actions }) {
|
||||
setIsModalOpen(false)
|
||||
setEditingId(null)
|
||||
setForm(emptyForm)
|
||||
setApiProbeLogin('')
|
||||
setApiProbePassword('')
|
||||
setTestConnectionResult(null)
|
||||
}}
|
||||
size="modal-md"
|
||||
>
|
||||
@@ -218,6 +271,88 @@ export function ProvidersPage({ db, actions }) {
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, notes: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<hr className="my-2" />
|
||||
<h6 className="text-secondary mb-2">Интеграция API (один URL на хостера)</h6>
|
||||
<div className="text-secondary small mb-2">
|
||||
Логин и пароль задаются в карточке каждого аккаунта этого хостера.
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label">Тип API</label>
|
||||
<select
|
||||
autoComplete="off"
|
||||
className="form-select"
|
||||
value={form.apiType}
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
apiType: e.target.value,
|
||||
apiBaseUrl: e.target.value === 'billmanager' ? prev.apiBaseUrl : '',
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="">— Не использовать —</option>
|
||||
<option value="billmanager">BILLmanager</option>
|
||||
</select>
|
||||
</div>
|
||||
{form.apiType === 'billmanager' ? (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<label className="form-label">URL API BILLmanager</label>
|
||||
<input
|
||||
{...noBrowserSuggestProps}
|
||||
className="form-control"
|
||||
placeholder="https://bill.example.com:1500/billmgr"
|
||||
value={form.apiBaseUrl}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, apiBaseUrl: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6">
|
||||
<label className="form-label">Тест: логин аккаунта</label>
|
||||
<input
|
||||
{...noBrowserSuggestProps}
|
||||
className="form-control"
|
||||
placeholder="не сохраняется"
|
||||
value={apiProbeLogin}
|
||||
onChange={(e) => setApiProbeLogin(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6">
|
||||
<label className="form-label">Тест: пароль</label>
|
||||
<input
|
||||
{...passwordCredentialInputProps}
|
||||
type="password"
|
||||
className="form-control"
|
||||
placeholder="не сохраняется"
|
||||
value={apiProbePassword}
|
||||
onChange={(e) => setApiProbePassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 d-flex align-items-center gap-2 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={onTestConnection}
|
||||
disabled={!canTestApi || testConnectionLoading}
|
||||
>
|
||||
{testConnectionLoading ? (
|
||||
<span className="spinner-border spinner-border-sm me-1" role="status" />
|
||||
) : (
|
||||
<IconPlugConnected size={14} className="me-1" />
|
||||
)}
|
||||
Проверить соединение
|
||||
</button>
|
||||
{testConnectionResult ? (
|
||||
<span className={testConnectionResult.ok ? 'text-success small' : 'text-danger small'}>
|
||||
{testConnectionResult.ok
|
||||
? `Соединение успешно${testConnectionResult.vdsCount != null ? `, VDS: ${testConnectionResult.vdsCount}` : ''}`
|
||||
: testConnectionResult.error}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div className="col-12 d-flex gap-2 justify-content-end">
|
||||
<button type="button" className="btn btn-outline-secondary" onClick={() => setIsModalOpen(false)}>
|
||||
Отмена
|
||||
|
||||
@@ -12,6 +12,7 @@ import { syncAccount } from '../lib/api'
|
||||
import { EmptyState } from '../components/EmptyState'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
import { noBrowserSuggestProps } from '../lib/noBrowserSuggestProps'
|
||||
import { billmanagerSyncableAccounts } from '../lib/billmanager-ui'
|
||||
|
||||
const SORT_COLUMNS = ['name', 'vcpu', 'ramGb', 'diskGb', 'diskType', 'virtualization', 'channel', 'country', 'location', 'price']
|
||||
|
||||
@@ -61,8 +62,8 @@ export function TariffsPage({ db, actions, settings, ratesData }) {
|
||||
const baseCurrency = (settings?.[0]?.baseCurrency || 'RUB').toUpperCase()
|
||||
|
||||
const billmanagerAccounts = useMemo(
|
||||
() => db.providerAccounts.filter((a) => a.apiType === 'billmanager' && a.apiBaseUrl),
|
||||
[db.providerAccounts],
|
||||
() => billmanagerSyncableAccounts(db.providerAccounts, db.providers),
|
||||
[db.providerAccounts, db.providers],
|
||||
)
|
||||
|
||||
const filteredAndSortedTariffs = useMemo(() => {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { PageHeader } from '../components/PageHeader'
|
||||
import { ProjectSuggestInput } from '../components/ProjectSuggestInput'
|
||||
import { noBrowserSuggestProps } from '../lib/noBrowserSuggestProps'
|
||||
import { getPaidUntilDate as computePaidUntilForHealth } from '../lib/paid-until'
|
||||
import { billmanagerSyncableAccounts } from '../lib/billmanager-ui'
|
||||
|
||||
const emptyForm = {
|
||||
ip: '',
|
||||
@@ -140,8 +141,8 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
||||
const baseCurrency = settings?.[0]?.baseCurrency || 'RUB'
|
||||
|
||||
const billmanagerAccounts = useMemo(
|
||||
() => db.providerAccounts.filter((a) => a.apiType === 'billmanager' && a.apiBaseUrl),
|
||||
[db.providerAccounts],
|
||||
() => billmanagerSyncableAccounts(db.providerAccounts, db.providers),
|
||||
[db.providerAccounts, db.providers],
|
||||
)
|
||||
|
||||
const providerById = useMemo(
|
||||
|
||||
Reference in New Issue
Block a user