删除dashboard_backup

This commit is contained in:
jingrow 2025-12-30 22:16:18 +08:00
parent 4e34df7876
commit 34fe7e70fe
705 changed files with 0 additions and 116792 deletions

View File

@ -1,2 +0,0 @@
defaults
not IE 11

View File

@ -1,14 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: ['plugin:vue/essential', 'eslint:recommended', '@vue/prettier'],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
};

View File

@ -1,22 +0,0 @@
.DS_Store
node_modules
/dist
/coverage
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,4 +0,0 @@
{
"singleQuote": true,
"useTabs": true
}

View File

@ -1,64 +0,0 @@
# Dashboard
Dashboard is a VueJS application that is the face of 今果 Jingrow. This is what the end users (tenants) see and manage their FC stuff in. The tenants does not have access to the desk, so, this is their dashboard for managing sites, apps, updates etc.
Technologies at the heart of dashboard:
1. [VueJS 3](https://vuejs.org/): The JavaScript framework of our choice.
2. [TailwindCSS 3](https://tailwindcss.com/): We love it.
3. [ViteJS](https://vitejs.dev/guide/): Build tooling for dev server and build command.
4. [Feather Icons](https://feathericons.com/): Those Shiny & Crisp Open Source icons.
## Development
We use the vite's development server, gives us super-fast hot reload and more.
### Running the development server
Run:
```bash
yarn run dev
```
> Note: If you are getting `CSRFTokenError` in your local development machine, please add the following key value pair in your site_cofig.json
>
> ```json
> "ignore_csrf": 1
> ```
### Proxy
While running the vite dev server, the requests to paths like `/app`, `/files` and `/api` are redirected to the actual site inside the bench. This makes sure these paths and other backend API keep working properly. You can check the [proxyOptions.js](./proxyOptions.js) files to check how the proxying happens. These options are then loaded and used in the [vite config](./vite.config.js) file.
## Testing
There is a separate setup for testing the frontend.
### The Stack
1. [MSW](https://mswjs.io/)
2. [Vitest](https://vitest.dev/)
### Running the tests
```bash
yarn run test
```
The tests run in CI too.
## Learning More
You can start by taking a look at the [main.js](./src/main.js) file. This is where the VueJS app is initialzed and the below things are attached (registered) to the instance:
1. Vue Router
2. Plugins
3. Controllers
4. Global Components
The logic to register each of the above is in its own separate file, you can take a look at the imports as required. Till we have a more docs, you have to dig into some `js` and `vue` files. If you find something that you can add here, feel free to raise a PR!

View File

@ -1,3 +0,0 @@
module.exports = {
presets: ['@babel/preset-env']
};

View File

@ -1,278 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
Alert: typeof import('./src/components/global/Alert.vue')['default']
AlertBenchUpdate: typeof import('./src/components/AlertBenchUpdate.vue')['default']
AlertBillingInformation: typeof import('./src/components/AlertBillingInformation.vue')['default']
AlertSiteActivation: typeof import('./src/components/AlertSiteActivation.vue')['default']
AlertSiteUpdate: typeof import('./src/components/AlertSiteUpdate.vue')['default']
AlertUpdate: typeof import('./src/components/AlertUpdate.vue')['default']
AlignCenter: typeof import('jingrow-ui/src/components/TextEditor/icons/align-center.vue')['default']
AlignJustify: typeof import('jingrow-ui/src/components/TextEditor/icons/align-justify.vue')['default']
AlignLeft: typeof import('jingrow-ui/src/components/TextEditor/icons/align-left.vue')['default']
AlignRight: typeof import('jingrow-ui/src/components/TextEditor/icons/align-right.vue')['default']
Amex: typeof import('./src/components/icons/cards/Amex.vue')['default']
AppPlanCard: typeof import('./src/components/AppPlanCard.vue')['default']
AppsIcon: typeof import('./src/components/icons/AppsIcon.vue')['default']
AppSourceSelector: typeof import('./src/components/AppSourceSelector.vue')['default']
AppSubscriptionSummary: typeof import('./src/components/AppSubscriptionSummary.vue')['default']
AppUpdateCard: typeof import('./src/components/AppUpdateCard.vue')['default']
ArrowGoBackLine: typeof import('jingrow-ui/src/components/TextEditor/icons/arrow-go-back-line.vue')['default']
ArrowGoForwardLine: typeof import('jingrow-ui/src/components/TextEditor/icons/arrow-go-forward-line.vue')['default']
Autocomplete: typeof import('jingrow-ui/src/components/Autocomplete.vue')['default']
'Autocomplete.story': typeof import('jingrow-ui/src/components/Autocomplete.story.vue')['default']
Avatar: typeof import('jingrow-ui/src/components/Avatar.vue')['default']
'Avatar.story': typeof import('jingrow-ui/src/components/Avatar.story.vue')['default']
AvatarUploader: typeof import('./src/components/AvatarUploader.vue')['default']
BackupFilesUploader: typeof import('./src/components/BackupFilesUploader.vue')['default']
Badge: typeof import('./src/components/global/Badge.vue')['default']
'Badge.story': typeof import('jingrow-ui/src/components/Badge.story.vue')['default']
BarChart: typeof import('./src/components/charts/BarChart.vue')['default']
BenchAppUpdates: typeof import('./src/components/BenchAppUpdates.vue')['default']
BenchIcon: typeof import('./src/components/icons/BenchIcon.vue')['default']
BenchSiteUpdates: typeof import('./src/components/BenchSiteUpdates.vue')['default']
BillingIcon: typeof import('./src/components/icons/BillingIcon.vue')['default']
BillingInformationDialog: typeof import('./src/components/BillingInformationDialog.vue')['default']
Bold: typeof import('jingrow-ui/src/components/TextEditor/icons/bold.vue')['default']
Breadcrumbs: typeof import('./src/components/global/Breadcrumbs.vue')['default']
'Breadcrumbs.story': typeof import('jingrow-ui/src/components/Breadcrumbs.story.vue')['default']
Button: typeof import('jingrow-ui/src/components/Button/Button.vue')['default']
'Button.story': typeof import('jingrow-ui/src/components/Button/Button.story.vue')['default']
BuyPrepaidCredits: typeof import('./src/components/BuyPrepaidCredits.vue')['default']
Calendar: typeof import('jingrow-ui/src/components/Calendar/Calendar.vue')['default']
'Calendar.story': typeof import('jingrow-ui/src/components/Calendar.story.vue')['default']
CalendarDaily: typeof import('jingrow-ui/src/components/Calendar/CalendarDaily.vue')['default']
CalendarEvent: typeof import('jingrow-ui/src/components/Calendar/CalendarEvent.vue')['default']
CalendarMonthly: typeof import('jingrow-ui/src/components/Calendar/CalendarMonthly.vue')['default']
CalendarTimeMarker: typeof import('jingrow-ui/src/components/Calendar/CalendarTimeMarker.vue')['default']
CalendarWeekly: typeof import('jingrow-ui/src/components/Calendar/CalendarWeekly.vue')['default']
Card: typeof import('./src/components/global/Card.vue')['default']
CardDetails: typeof import('./src/components/CardDetails.vue')['default']
CardWithDetails: typeof import('./src/components/CardWithDetails.vue')['default']
ChangeAppBranchDialog: typeof import('./src/components/ChangeAppBranchDialog.vue')['default']
ChangeAppPlanSelector: typeof import('./src/components/ChangeAppPlanSelector.vue')['default']
ChangePaymentModeDialog: typeof import('./src/components/ChangePaymentModeDialog.vue')['default']
Checkbox: typeof import('jingrow-ui/src/components/Checkbox.vue')['default']
'Checkbox.story': typeof import('jingrow-ui/src/components/Checkbox.story.vue')['default']
CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
CircularCheckIcon: typeof import('./src/components/global/CircularCheckIcon.vue')['default']
CircularProgressBar: typeof import('jingrow-ui/src/components/CircularProgressBar.vue')['default']
'CircularProgressBar.story': typeof import('jingrow-ui/src/components/CircularProgressBar.story.vue')['default']
ClickToCopyField: typeof import('./src/components/ClickToCopyField.vue')['default']
CodeView: typeof import('jingrow-ui/src/components/TextEditor/icons/code-view.vue')['default']
CommandPalette: typeof import('./src/components/CommandPalette.vue')['default']
CommandPaletteItem: typeof import('jingrow-ui/src/components/CommandPalette/CommandPaletteItem.vue')['default']
CommitChooser: typeof import('./src/components/utils/CommitChooser.vue')['default']
CommitTag: typeof import('./src/components/utils/CommitTag.vue')['default']
ConfigEditor: typeof import('./src/components/ConfigEditor.vue')['default']
ConfirmDialog: typeof import('jingrow-ui/src/components/ConfirmDialog.vue')['default']
ConfirmDialogs: typeof import('./src/components/ConfirmDialogs.vue')['default']
CreateAppVersionDialog: typeof import('./src/components/marketplace/CreateAppVersionDialog.vue')['default']
DatePicker: typeof import('jingrow-ui/src/components/DatePicker/DatePicker.vue')['default']
'DatePicker.story': typeof import('jingrow-ui/src/components/DatePicker/DatePicker.story.vue')['default']
DateRangePicker: typeof import('jingrow-ui/src/components/DatePicker/DateRangePicker.vue')['default']
DateTimePicker: typeof import('jingrow-ui/src/components/DatePicker/DateTimePicker.vue')['default']
DescriptionList: typeof import('./src/components/DescriptionList.vue')['default']
Dialog: typeof import('jingrow-ui/src/components/Dialog.vue')['default']
'Dialog.story': typeof import('jingrow-ui/src/components/Dialog.story.vue')['default']
Dialogs: typeof import('jingrow-ui/src/components/Dialogs.vue')['default']
Divider: typeof import('jingrow-ui/src/components/Divider.vue')['default']
DoubleQuotesR: typeof import('jingrow-ui/src/components/TextEditor/icons/double-quotes-r.vue')['default']
Dropdown: typeof import('jingrow-ui/src/components/Dropdown.vue')['default']
'Dropdown.story': typeof import('jingrow-ui/src/components/Dropdown.story.vue')['default']
ErrorMessage: typeof import('jingrow-ui/src/components/ErrorMessage.vue')['default']
'ErrorMessage.story': typeof import('jingrow-ui/src/components/ErrorMessage.story.vue')['default']
EventModalContent: typeof import('jingrow-ui/src/components/Calendar/EventModalContent.vue')['default']
FeatherIcon: typeof import('jingrow-ui/src/components/FeatherIcon.vue')['default']
FeatureList: typeof import('./src/components/FeatureList.vue')['default']
FileUploader: typeof import('./src/components/FileUploader.vue')['default']
'FileUploader.story': typeof import('jingrow-ui/src/components/FileUploader.story.vue')['default']
FilterIcon: typeof import('jingrow-ui/src/components/ListFilter/FilterIcon.vue')['default']
FloatingPopover: typeof import('jingrow-ui/src/components/Calendar/FloatingPopover.vue')['default']
FontColor: typeof import('jingrow-ui/src/components/TextEditor/FontColor.vue')['default']
Form: typeof import('./src/components/Form.vue')['default']
FormatClear: typeof import('jingrow-ui/src/components/TextEditor/icons/format-clear.vue')['default']
FormatDate: typeof import('./src/components/global/FormatDate.vue')['default']
FormControl: typeof import('jingrow-ui/src/components/FormControl.vue')['default']
'FormControl.story': typeof import('jingrow-ui/src/components/FormControl.story.vue')['default']
FrappeCloudLogo: typeof import('./src/components/FrappeCloudLogo.vue')['default']
Generic: typeof import('./src/components/icons/cards/Generic.vue')['default']
GoogleIcon: typeof import('./src/components/icons/GoogleIcon.vue')['default']
GoogleIconSolid: typeof import('./src/components/icons/GoogleIconSolid.vue')['default']
GrayCheckIcon: typeof import('./src/components/global/GrayCheckIcon.vue')['default']
GreenCheckIcon: typeof import('jingrow-ui/src/components/GreenCheckIcon.vue')['default']
H1: typeof import('jingrow-ui/src/components/TextEditor/icons/h-1.vue')['default']
H2: typeof import('jingrow-ui/src/components/TextEditor/icons/h-2.vue')['default']
H3: typeof import('jingrow-ui/src/components/TextEditor/icons/h-3.vue')['default']
H4: typeof import('jingrow-ui/src/components/TextEditor/icons/h-4.vue')['default']
H5: typeof import('jingrow-ui/src/components/TextEditor/icons/h-5.vue')['default']
H6: typeof import('jingrow-ui/src/components/TextEditor/icons/h-6.vue')['default']
ILucideAlertCircle: typeof import('~icons/lucide/alert-circle')['default']
ILucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
ILucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
ILucideBadgeCheck: typeof import('~icons/lucide/badge-check')['default']
ILucideCheck: typeof import('~icons/lucide/check')['default']
ILucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
ILucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
ILucideClock: typeof import('~icons/lucide/clock')['default']
ILucideCopy: typeof import('~icons/lucide/copy')['default']
ILucideExternalLink: typeof import('~icons/lucide/external-link')['default']
ILucideFileText: typeof import('~icons/lucide/file-text')['default']
ILucideFilter: typeof import('~icons/lucide/filter')['default']
ILucideFlaskConical: typeof import('~icons/lucide/flask-conical')['default']
ILucideGithub: typeof import('~icons/lucide/github')['default']
ILucideGlobe: typeof import('~icons/lucide/globe')['default']
ILucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
ILucideInfo: typeof import('~icons/lucide/info')['default']
ILucideMenu: typeof import('~icons/lucide/menu')['default']
ILucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
ILucidePlus: typeof import('~icons/lucide/plus')['default']
ILucideRefreshCcw: typeof import('~icons/lucide/refresh-ccw')['default']
ILucideSearch: typeof import('~icons/lucide/search')['default']
ILucideSparkle: typeof import('~icons/lucide/sparkle')['default']
ILucideX: typeof import('~icons/lucide/x')['default']
ImageAddLine: typeof import('jingrow-ui/src/components/TextEditor/icons/image-add-line.vue')['default']
ImpersonateTeam: typeof import('./src/components/ImpersonateTeam.vue')['default']
Input: typeof import('jingrow-ui/src/components/Input.vue')['default']
InsertImage: typeof import('jingrow-ui/src/components/TextEditor/InsertImage.vue')['default']
InsertLink: typeof import('jingrow-ui/src/components/TextEditor/InsertLink.vue')['default']
InsertVideo: typeof import('jingrow-ui/src/components/TextEditor/InsertVideo.vue')['default']
InvoiceUsageCard: typeof import('./src/components/InvoiceUsageCard.vue')['default']
InvoiceUsageTable: typeof import('./src/components/InvoiceUsageTable.vue')['default']
Italic: typeof import('jingrow-ui/src/components/TextEditor/icons/italic.vue')['default']
JCB: typeof import('./src/components/icons/cards/JCB.vue')['default']
JcloudLogo: typeof import('./src/components/icons/JcloudLogo.vue')['default']
JingrowLogo: typeof import('./src/components/icons/JingrowLogo.vue')['default']
JLogo: typeof import('./src/components/icons/JLogo.vue')['default']
LightningIcon: typeof import('jingrow-ui/src/components/Billing/LightningIcon.vue')['default']
LineChart: typeof import('./src/components/charts/LineChart.vue')['default']
Link: typeof import('./src/components/Link.vue')['default']
ListEmptyState: typeof import('jingrow-ui/src/components/ListView/ListEmptyState.vue')['default']
ListFilter: typeof import('jingrow-ui/src/components/ListFilter/ListFilter.vue')['default']
ListFooter: typeof import('jingrow-ui/src/components/ListView/ListFooter.vue')['default']
ListGroupHeader: typeof import('jingrow-ui/src/components/ListView/ListGroupHeader.vue')['default']
ListGroupRows: typeof import('jingrow-ui/src/components/ListView/ListGroupRows.vue')['default']
ListGroups: typeof import('jingrow-ui/src/components/ListView/ListGroups.vue')['default']
ListHeader: typeof import('jingrow-ui/src/components/ListView/ListHeader.vue')['default']
ListHeaderItem: typeof import('jingrow-ui/src/components/ListView/ListHeaderItem.vue')['default']
ListItem: typeof import('./src/components/global/ListItem.vue')['default']
ListOrdered: typeof import('jingrow-ui/src/components/TextEditor/icons/list-ordered.vue')['default']
ListRow: typeof import('jingrow-ui/src/components/ListView/ListRow.vue')['default']
ListRowItem: typeof import('jingrow-ui/src/components/ListView/ListRowItem.vue')['default']
ListRows: typeof import('jingrow-ui/src/components/ListView/ListRows.vue')['default']
ListSelectBanner: typeof import('jingrow-ui/src/components/ListView/ListSelectBanner.vue')['default']
ListUnordered: typeof import('jingrow-ui/src/components/TextEditor/icons/list-unordered.vue')['default']
ListView: typeof import('jingrow-ui/src/components/ListView/ListView.vue')['default']
'ListView.story': typeof import('jingrow-ui/src/components/ListView/ListView.story.vue')['default']
LoadingIndicator: typeof import('jingrow-ui/src/components/LoadingIndicator.vue')['default']
LoadingText: typeof import('jingrow-ui/src/components/LoadingText.vue')['default']
MarketplaceAppCard: typeof import('./src/components/MarketplaceAppCard.vue')['default']
MarketplaceAppDescriptions: typeof import('./src/components/MarketplaceAppDescriptions.vue')['default']
MarketplaceAppLinks: typeof import('./src/components/MarketplaceAppLinks.vue')['default']
MarketplaceAppProfile: typeof import('./src/components/MarketplaceAppProfile.vue')['default']
MarketplaceAppReleaseList: typeof import('./src/components/MarketplaceAppReleaseList.vue')['default']
MarketplaceAppReviewStages: typeof import('./src/components/marketplace/MarketplaceAppReviewStages.vue')['default']
MarketplaceAppScreenshots: typeof import('./src/components/MarketplaceAppScreenshots.vue')['default']
MasterCard: typeof import('./src/components/icons/cards/MasterCard.vue')['default']
MentionList: typeof import('jingrow-ui/src/components/TextEditor/MentionList.vue')['default']
Menu: typeof import('jingrow-ui/src/components/TextEditor/Menu.vue')['default']
Modal: typeof import('./src/components/Modal.vue')['default']
Navbar: typeof import('./src/components/Navbar.vue')['default']
NestedPopover: typeof import('jingrow-ui/src/components/ListFilter/NestedPopover.vue')['default']
NewAppRepositories: typeof import('./src/components/NewAppRepositories.vue')['default']
NewEventModal: typeof import('jingrow-ui/src/components/Calendar/NewEventModal.vue')['default']
Notification: typeof import('./src/components/Notification.vue')['default']
NotificationToasts: typeof import('./src/components/NotificationToasts.vue')['default']
OldDropdown: typeof import('./src/components/global/OldDropdown.vue')['default']
PageHeader: typeof import('./src/components/PageHeader.vue')['default']
PlanIcon: typeof import('./src/components/PlanIcon.vue')['default']
Popover: typeof import('./src/components/Popover.vue')['default']
'Popover.story': typeof import('jingrow-ui/src/components/Popover.story.vue')['default']
PrepaidCreditsDialog: typeof import('./src/components/PrepaidCreditsDialog.vue')['default']
PrinterIcon: typeof import('./src/components/PrinterIcon.vue')['default']
Progress: typeof import('jingrow-ui/src/components/Progress.vue')['default']
'Progress.story': typeof import('jingrow-ui/src/components/Progress.story.vue')['default']
ProgressArc: typeof import('./src/components/ProgressArc.vue')['default']
PublisherPayoutInfoCard: typeof import('./src/components/marketplace/PublisherPayoutInfoCard.vue')['default']
PublisherProfileCard: typeof import('./src/components/marketplace/PublisherProfileCard.vue')['default']
Rating: typeof import('jingrow-ui/src/components/Rating/Rating.vue')['default']
'Rating.story': typeof import('jingrow-ui/src/components/Rating/Rating.story.vue')['default']
Report: typeof import('./src/components/Report.vue')['default']
Resource: typeof import('jingrow-ui/src/components/Resource.vue')['default']
RichSelect: typeof import('./src/components/RichSelect.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchComplete: typeof import('jingrow-ui/src/components/ListFilter/SearchComplete.vue')['default']
Section: typeof import('./src/components/global/Section.vue')['default']
SectionCard: typeof import('./src/components/global/SectionCard.vue')['default']
SectionHeader: typeof import('./src/components/global/SectionHeader.vue')['default']
SecurityIcon: typeof import('./src/components/icons/SecurityIcon.vue')['default']
Select: typeof import('./src/components/global/Select.vue')['default']
'Select.story': typeof import('jingrow-ui/src/components/Select.story.vue')['default']
SelectableCard: typeof import('./src/components/SelectableCard.vue')['default']
SelectAppFromGithub: typeof import('./src/components/SelectAppFromGithub.vue')['default']
Separator: typeof import('jingrow-ui/src/components/TextEditor/icons/separator.vue')['default']
ServerIcon: typeof import('./src/components/icons/ServerIcon.vue')['default']
ServerPlansTable: typeof import('./src/components/ServerPlansTable.vue')['default']
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
ShowMoreCalendarEvent: typeof import('jingrow-ui/src/components/Calendar/ShowMoreCalendarEvent.vue')['default']
Sidebar: typeof import('./src/components/Sidebar.vue')['default']
SiteAppUpdateCard: typeof import('./src/components/SiteAppUpdateCard.vue')['default']
SiteAppUpdates: typeof import('./src/components/SiteAppUpdates.vue')['default']
SiteIcon: typeof import('./src/components/icons/SiteIcon.vue')['default']
SitePlansTable: typeof import('./src/components/SitePlansTable.vue')['default']
SiteRestoreSelector: typeof import('./src/components/SiteRestoreSelector.vue')['default']
SiteUpdateCard: typeof import('./src/components/SiteUpdateCard.vue')['default']
SpacesIcon: typeof import('./src/components/icons/SpacesIcon.vue')['default']
Spinner: typeof import('./src/components/global/Spinner.vue')['default']
'Spinner.story': typeof import('jingrow-ui/src/components/Spinner.story.vue')['default']
StarRatingInput: typeof import('./src/components/StarRatingInput.vue')['default']
Steps: typeof import('./src/components/Steps.vue')['default']
Strikethrough: typeof import('jingrow-ui/src/components/TextEditor/icons/strikethrough.vue')['default']
StripeCard: typeof import('./src/components/StripeCard.vue')['default']
StripeLogo: typeof import('./src/components/StripeLogo.vue')['default']
SuccessCard: typeof import('./src/components/global/SuccessCard.vue')['default']
Switch: typeof import('jingrow-ui/src/components/Switch.vue')['default']
'Switch.story': typeof import('jingrow-ui/src/components/Switch.story.vue')['default']
SwitchTeamDialog: typeof import('./src/components/SwitchTeamDialog.vue')['default']
TabButtons: typeof import('jingrow-ui/src/components/TabButtons.vue')['default']
'TabButtons.story': typeof import('jingrow-ui/src/components/TabButtons.story.vue')['default']
Table: typeof import('./src/components/Table/Table.vue')['default']
Table2: typeof import('jingrow-ui/src/components/TextEditor/icons/table-2.vue')['default']
TableCell: typeof import('./src/components/Table/TableCell.vue')['default']
TableHeader: typeof import('./src/components/Table/TableHeader.vue')['default']
TableRow: typeof import('./src/components/Table/TableRow.vue')['default']
TabList: typeof import('jingrow-ui/src/components/Tabs/TabList.vue')['default']
TabPanel: typeof import('jingrow-ui/src/components/Tabs/TabPanel.vue')['default']
Tabs: typeof import('./src/components/Tabs.vue')['default']
'Tabs.story': typeof import('jingrow-ui/src/components/Tabs/Tabs.story.vue')['default']
Tags: typeof import('./src/components/Tags.vue')['default']
Text: typeof import('jingrow-ui/src/components/TextEditor/icons/text.vue')['default']
Textarea: typeof import('jingrow-ui/src/components/Textarea.vue')['default']
'Textarea.story': typeof import('jingrow-ui/src/components/Textarea.story.vue')['default']
TextEditor: typeof import('jingrow-ui/src/components/TextEditor/TextEditor.vue')['default']
'TextEditor.story': typeof import('jingrow-ui/src/components/TextEditor/TextEditor.story.vue')['default']
TextEditorBubbleMenu: typeof import('jingrow-ui/src/components/TextEditor/TextEditorBubbleMenu.vue')['default']
TextEditorFixedMenu: typeof import('jingrow-ui/src/components/TextEditor/TextEditorFixedMenu.vue')['default']
TextEditorFloatingMenu: typeof import('jingrow-ui/src/components/TextEditor/TextEditorFloatingMenu.vue')['default']
TextInput: typeof import('jingrow-ui/src/components/TextInput.vue')['default']
'TextInput.story': typeof import('jingrow-ui/src/components/TextInput.story.vue')['default']
Toast: typeof import('jingrow-ui/src/components/Toast.vue')['default']
Tooltip: typeof import('jingrow-ui/src/components/Tooltip/Tooltip.vue')['default']
'Tooltip.story': typeof import('jingrow-ui/src/components/Tooltip/Tooltip.story.vue')['default']
Tree: typeof import('jingrow-ui/src/components/Tree/Tree.vue')['default']
'Tree.story': typeof import('jingrow-ui/src/components/Tree/Tree.story.vue')['default']
TrialBanner: typeof import('jingrow-ui/src/components/Billing/TrialBanner.vue')['default']
Underline: typeof import('jingrow-ui/src/components/TextEditor/icons/underline.vue')['default']
UnionPay: typeof import('./src/components/icons/cards/UnionPay.vue')['default']
VideoAddLine: typeof import('jingrow-ui/src/components/TextEditor/icons/video-add-line.vue')['default']
Visa: typeof import('./src/components/icons/cards/Visa.vue')['default']
WizardCard: typeof import('./src/components/WizardCard.vue')['default']
}
}

View File

@ -1,10 +0,0 @@
/**
* This node script resolves the tailwind config and dumps it as a json in
* tailwind.theme.json which is later imported into the app.
*/
let fs = require('fs');
let resolveConfig = require('tailwindcss/resolveConfig');
let config = require('./tailwind.config.cjs');
let { theme } = resolveConfig(config);
fs.writeFileSync('./tailwind.theme.json', JSON.stringify(theme, null, 2));

View File

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html class="h-full overflow-hidden" lang="zh">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>今果 Jingrow</title>
<link rel="icon" href="/favicon.png" type="image/x-icon" />
</head>
<body class="h-full">
<noscript>
<strong>
今果 Jingrow Dashboard doesn't work properly without JavaScript enabled.
Please enable it to continue.
</strong>
</noscript>
<!-- Main Vue App -->
<div id="app" class="h-full"></div>
<!-- For Teleports -->
<div id="modals"></div>
<div id="popovers"></div>
<script>
{% for key in boot %}
window["{{ key }}"] = {{ boot[key] | tojson }};
{% endfor %}
</script>
<!-- <script type="module" src="/src/main.js"></script> -->
<script type="module" src="/src2/main.js"></script>
</body>
</html>

View File

@ -1,9 +0,0 @@
{
"include": ["./src/**/*", "src2/components/AddressableErrorDialog.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

View File

@ -1,95 +0,0 @@
{
"name": "dashboard",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "yarn generate-theme-config && vite",
"build": "yarn generate-theme-config && vite build --base=/assets/jcloud/dashboard/ && yarn copy-html-entry",
"copy-html-entry": "cp ../jcloud/public/dashboard/index.html ../jcloud/www/dashboard.html",
"generate-theme-config": "node ./generateThemeConfig.cjs",
"test": "vitest",
"coverage": "vitest run --coverage",
"lint": "eslint src"
},
"dependencies": {
"@codemirror/autocomplete": "^6.18.1",
"@codemirror/lang-sql": "^6.8.0",
"@headlessui/vue": "^1.7.14",
"@popperjs/core": "^2.11.2",
"@sentry/vite-plugin": "^2.19.0",
"@sentry/vue": "^8.10.0",
"@stripe/stripe-js": "^1.3.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/vue-table": "^8.20.5",
"@vueuse/components": "^10.7.0",
"@vueuse/core": "^10.3.0",
"codemirror": "^6.0.1",
"core-js": "^3.6.4",
"dayjs": "^1.10.7",
"echarts": "^5.4.3",
"feather-icons": "^4.26.0",
"jingrow-charts": "http://npm.jingrow.com:105/jingrow-charts-2.0.0-rc22.tgz",
"jingrow-ui": "http://npm.jingrow.com:105/jingrow-ui-1.0.1.tgz",
"fuse.js": "6.6.2",
"libarchive.js": "^1.3.0",
"lodash": "^4.17.19",
"luxon": "^1.22.0",
"markdown-it": "^12.3.2",
"papaparse": "^5.4.1",
"qrcode": "^1.5.4",
"register-service-worker": "^1.6.2",
"socket.io-client": "^4.5.1",
"sql-formatter": "^15.4.10",
"unplugin-icons": "^0.17.0",
"unplugin-vue-components": "^0.25.2",
"vue": "^3.4.12",
"vue-codemirror": "^6.1.1",
"vue-echarts": "^6.6.1",
"vue-qrcode": "^2.2.2",
"vue-router": "^4.0.5",
"vue-sonner": "^1.2.5"
},
"devDependencies": {
"@iconify/json": "^2.2.123",
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/postcss7-compat": "^2.0.2",
"@tailwindcss/typography": "^0.5.1",
"@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue": "^5.0.3",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/compiler-sfc": "^3.1.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "^2.0.0-rc.19",
"autoprefixer": "^10.4.2",
"babel-eslint": "^10.0.3",
"c8": "^7.11.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^6.2.2",
"jsdom": "^19.0.0",
"lint-staged": "^9.5.0",
"msw": "^0.36.8",
"node-fetch": "^3.2.10",
"postcss": "^8.4.6",
"postcss-easy-import": "^4.0.0",
"prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.8",
"tailwindcss": "^3.2",
"typescript": "^5.4.3",
"vite": "5.0.13",
"vite-plugin-rewrite-all": "^1.0.1",
"vitest": "^0.9.3",
"vue-tsc": "^2.0.7",
"yorkie": "^2.0.0"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,vue}": [
"yarn lint",
"git add"
]
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,2 +0,0 @@
User-agent: *
Disallow:

View File

@ -1,7 +0,0 @@
declare global {
interface Window {
is_system_user?: boolean;
}
}
export {};

View File

@ -1,5 +0,0 @@
declare module '~icons/*' {
import { FunctionalComponent, SVGAttributes } from 'vue';
const component: FunctionalComponent<SVGAttributes>;
export default component;
}

View File

@ -1,74 +0,0 @@
<template>
<div class="text-gray-900 antialiased">
<div class="flex h-screen overflow-hidden">
<div
class="flex flex-1 overflow-y-auto"
:class="{
'sm:bg-gray-50':
$route.meta.isLoginPage && $route.fullPath.indexOf('/checkout') < 0
}"
>
<div class="flex-1">
<Navbar class="sm:hidden" v-if="!$route.meta.isLoginPage" />
<div class="mx-auto flex flex-row justify-start">
<Sidebar
class="sticky top-0 hidden w-64 flex-shrink-0 sm:flex"
v-if="$auth.isLoggedIn && !$route.meta.hideSidebar"
/>
<router-view v-slot="{ Component }" class="w-full sm:mr-0">
<keep-alive
:include="[
'Sites',
'Benches',
'Servers',
'Site',
'Bench',
'Server',
'Marketplace',
'Account',
'MarketplaceApp'
]"
>
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</div>
</div>
</div>
<NotificationToasts />
<UserPrompts v-if="$auth.isLoggedIn" />
<ConfirmDialogs />
</div>
</template>
<script>
import Sidebar from '@/components/Sidebar.vue';
import Navbar from '@/components/Navbar.vue';
import UserPrompts from '@/views/onboarding/UserPrompts.vue';
import ConfirmDialogs from '@/components/ConfirmDialogs.vue';
import NotificationToasts from '@/components/NotificationToasts.vue';
export default {
name: 'App',
components: {
Sidebar,
Navbar,
UserPrompts,
ConfirmDialogs,
NotificationToasts
},
data() {
return {
viewportWidth: 0
};
},
provide: {
viewportWidth: Math.max(
document.documentElement.clientWidth || 0,
window.innerWidth || 0
)
}
};
</script>
<style src="./assets/style.css"></style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

@ -1,152 +0,0 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url('Inter-Thin.woff2?v=3.12') format('woff2'),
url('Inter-Thin.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url('Inter-ThinItalic.woff2?v=3.12') format('woff2'),
url('Inter-ThinItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url('Inter-ExtraLight.woff2?v=3.12') format('woff2'),
url('Inter-ExtraLight.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url('Inter-ExtraLightItalic.woff2?v=3.12') format('woff2'),
url('Inter-ExtraLightItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('Inter-Light.woff2?v=3.12') format('woff2'),
url('Inter-Light.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('Inter-LightItalic.woff2?v=3.12') format('woff2'),
url('Inter-LightItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('Inter-Regular.woff2?v=3.12') format('woff2'),
url('Inter-Regular.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('Inter-Italic.woff2?v=3.12') format('woff2'),
url('Inter-Italic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('Inter-Medium.woff2?v=3.12') format('woff2'),
url('Inter-Medium.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('Inter-MediumItalic.woff2?v=3.12') format('woff2'),
url('Inter-MediumItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('Inter-SemiBold.woff2?v=3.12') format('woff2'),
url('Inter-SemiBold.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('Inter-SemiBoldItalic.woff2?v=3.12') format('woff2'),
url('Inter-SemiBoldItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('Inter-Bold.woff2?v=3.12') format('woff2'),
url('Inter-Bold.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('Inter-BoldItalic.woff2?v=3.12') format('woff2'),
url('Inter-BoldItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('Inter-ExtraBold.woff2?v=3.12') format('woff2'),
url('Inter-ExtraBold.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('Inter-ExtraBoldItalic.woff2?v=3.12') format('woff2'),
url('Inter-ExtraBoldItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url('Inter-Black.woff2?v=3.12') format('woff2'),
url('Inter-Black.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url('Inter-BlackItalic.woff2?v=3.12') format('woff2'),
url('Inter-BlackItalic.woff?v=3.12') format('woff');
}

View File

@ -1,162 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0.00 0.00 128.00 128.00">
<g stroke-width="2.00" fill="none" stroke-linecap="butt">
<path stroke="#8fe3b1" vector-effect="non-scaling-stroke" d="
M 64.04 34.07
Q 64.93 34.07 65.55 34.69
Q 74.55 43.60 82.91 51.96
C 87.46 56.52 91.92 55.95 98.45 55.81
A 2.52 2.52 0.0 0 0 100.92 53.29
L 100.92 49.89
A 2.56 2.55 -1.5 0 0 98.23 47.34
C 95.84 47.46 91.15 47.62 89.22 45.67
Q 81.61 37.99 68.36 24.99
Q 66.43 23.09 64.04 23.09
Q 61.66 23.09 59.72 24.99
Q 46.47 37.98 38.85 45.66
C 36.92 47.61 32.23 47.45 29.84 47.32
A 2.56 2.55 1.5 0 0 27.15 49.87
L 27.15 53.27
A 2.52 2.52 0.0 0 0 29.62 55.79
C 36.15 55.94 40.61 56.51 45.16 51.95
Q 53.53 43.60 62.53 34.69
Q 63.15 34.07 64.04 34.07"
/>
<path stroke="#8fe3b1" vector-effect="non-scaling-stroke" d="
M 64.04 101.46
C 66.42 101.46 68.47 101.10 68.47 98.24
Q 68.49 75.28 68.49 72.56
A 0.61 0.61 0.0 0 1 69.10 71.95
L 89.64 71.95
A 2.50 2.50 0.0 0 0 92.14 69.45
L 92.14 65.73
A 2.28 2.28 0.0 0 0 89.86 63.45
L 69.10 63.45
A 0.61 0.61 0.0 0 1 68.49 62.84
L 68.49 50.37
A 3.05 3.03 2.1 0 0 65.67 47.34
Q 65.14 47.31 64.03 47.31
Q 62.93 47.31 62.40 47.34
A 3.05 3.03 -2.1 0 0 59.58 50.37
L 59.58 62.84
A 0.61 0.61 0.0 0 1 58.97 63.45
L 38.21 63.45
A 2.28 2.28 0.0 0 0 35.93 65.73
L 35.93 69.45
A 2.50 2.50 0.0 0 0 38.43 71.95
L 58.97 71.95
A 0.61 0.61 0.0 0 1 59.58 72.56
Q 59.58 75.28 59.60 98.24
C 59.60 101.10 61.65 101.46 64.04 101.46"
/>
</g>
<path fill="#1fc76f" d="
M 115.34 95.08
A 19.82 19.82 0.0 0 1 95.52 114.90
L 32.48 114.90
A 19.82 19.82 0.0 0 1 12.66 95.08
L 12.66 32.88
A 19.82 19.82 0.0 0 1 32.48 13.06
L 95.52 13.06
A 19.82 19.82 0.0 0 1 115.34 32.88
L 115.34 95.08
Z
M 64.04 34.07
Q 64.93 34.07 65.55 34.69
Q 74.55 43.60 82.91 51.96
C 87.46 56.52 91.92 55.95 98.45 55.81
A 2.52 2.52 0.0 0 0 100.92 53.29
L 100.92 49.89
A 2.56 2.55 -1.5 0 0 98.23 47.34
C 95.84 47.46 91.15 47.62 89.22 45.67
Q 81.61 37.99 68.36 24.99
Q 66.43 23.09 64.04 23.09
Q 61.66 23.09 59.72 24.99
Q 46.47 37.98 38.85 45.66
C 36.92 47.61 32.23 47.45 29.84 47.32
A 2.56 2.55 1.5 0 0 27.15 49.87
L 27.15 53.27
A 2.52 2.52 0.0 0 0 29.62 55.79
C 36.15 55.94 40.61 56.51 45.16 51.95
Q 53.53 43.60 62.53 34.69
Q 63.15 34.07 64.04 34.07
Z
M 64.04 101.46
C 66.42 101.46 68.47 101.10 68.47 98.24
Q 68.49 75.28 68.49 72.56
A 0.61 0.61 0.0 0 1 69.10 71.95
L 89.64 71.95
A 2.50 2.50 0.0 0 0 92.14 69.45
L 92.14 65.73
A 2.28 2.28 0.0 0 0 89.86 63.45
L 69.10 63.45
A 0.61 0.61 0.0 0 1 68.49 62.84
L 68.49 50.37
A 3.05 3.03 2.1 0 0 65.67 47.34
Q 65.14 47.31 64.03 47.31
Q 62.93 47.31 62.40 47.34
A 3.05 3.03 -2.1 0 0 59.58 50.37
L 59.58 62.84
A 0.61 0.61 0.0 0 1 58.97 63.45
L 38.21 63.45
A 2.28 2.28 0.0 0 0 35.93 65.73
L 35.93 69.45
A 2.50 2.50 0.0 0 0 38.43 71.95
L 58.97 71.95
A 0.61 0.61 0.0 0 1 59.58 72.56
Q 59.58 75.28 59.60 98.24
C 59.60 101.10 61.65 101.46 64.04 101.46
Z"
/>
<path fill="#fffef2" d="
M 64.04 23.09
Q 66.43 23.09 68.36 24.99
Q 81.61 37.99 89.22 45.67
C 91.15 47.62 95.84 47.46 98.23 47.34
A 2.56 2.55 -1.5 0 1 100.92 49.89
L 100.92 53.29
A 2.52 2.52 0.0 0 1 98.45 55.81
C 91.92 55.95 87.46 56.52 82.91 51.96
Q 74.55 43.60 65.55 34.69
Q 64.93 34.07 64.04 34.07
Q 63.15 34.07 62.53 34.69
Q 53.53 43.60 45.16 51.95
C 40.61 56.51 36.15 55.94 29.62 55.79
A 2.52 2.52 0.0 0 1 27.15 53.27
L 27.15 49.87
A 2.56 2.55 1.5 0 1 29.84 47.32
C 32.23 47.45 36.92 47.61 38.85 45.66
Q 46.47 37.98 59.72 24.99
Q 61.66 23.09 64.04 23.09
Z"
/>
<path fill="#fffef2" d="
M 64.03 47.31
Q 65.14 47.31 65.67 47.34
A 3.05 3.03 2.1 0 1 68.49 50.37
L 68.49 62.84
A 0.61 0.61 0.0 0 0 69.10 63.45
L 89.86 63.45
A 2.28 2.28 0.0 0 1 92.14 65.73
L 92.14 69.45
A 2.50 2.50 0.0 0 1 89.64 71.95
L 69.10 71.95
A 0.61 0.61 0.0 0 0 68.49 72.56
Q 68.49 75.28 68.47 98.24
C 68.47 101.10 66.42 101.46 64.04 101.46
C 61.65 101.46 59.60 101.10 59.60 98.24
Q 59.58 75.28 59.58 72.56
A 0.61 0.61 0.0 0 0 58.97 71.95
L 38.43 71.95
A 2.50 2.50 0.0 0 1 35.93 69.45
L 35.93 65.73
A 2.28 2.28 0.0 0 1 38.21 63.45
L 58.97 63.45
A 0.61 0.61 0.0 0 0 59.58 62.84
L 59.58 50.37
A 3.05 3.03 -2.1 0 1 62.40 47.34
Q 62.93 47.31 64.03 47.31
Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,162 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0.00 0.00 128.00 128.00">
<g stroke-width="2.00" fill="none" stroke-linecap="butt">
<path stroke="#8fe3b1" vector-effect="non-scaling-stroke" d="
M 64.04 34.07
Q 64.93 34.07 65.55 34.69
Q 74.55 43.60 82.91 51.96
C 87.46 56.52 91.92 55.95 98.45 55.81
A 2.52 2.52 0.0 0 0 100.92 53.29
L 100.92 49.89
A 2.56 2.55 -1.5 0 0 98.23 47.34
C 95.84 47.46 91.15 47.62 89.22 45.67
Q 81.61 37.99 68.36 24.99
Q 66.43 23.09 64.04 23.09
Q 61.66 23.09 59.72 24.99
Q 46.47 37.98 38.85 45.66
C 36.92 47.61 32.23 47.45 29.84 47.32
A 2.56 2.55 1.5 0 0 27.15 49.87
L 27.15 53.27
A 2.52 2.52 0.0 0 0 29.62 55.79
C 36.15 55.94 40.61 56.51 45.16 51.95
Q 53.53 43.60 62.53 34.69
Q 63.15 34.07 64.04 34.07"
/>
<path stroke="#8fe3b1" vector-effect="non-scaling-stroke" d="
M 64.04 101.46
C 66.42 101.46 68.47 101.10 68.47 98.24
Q 68.49 75.28 68.49 72.56
A 0.61 0.61 0.0 0 1 69.10 71.95
L 89.64 71.95
A 2.50 2.50 0.0 0 0 92.14 69.45
L 92.14 65.73
A 2.28 2.28 0.0 0 0 89.86 63.45
L 69.10 63.45
A 0.61 0.61 0.0 0 1 68.49 62.84
L 68.49 50.37
A 3.05 3.03 2.1 0 0 65.67 47.34
Q 65.14 47.31 64.03 47.31
Q 62.93 47.31 62.40 47.34
A 3.05 3.03 -2.1 0 0 59.58 50.37
L 59.58 62.84
A 0.61 0.61 0.0 0 1 58.97 63.45
L 38.21 63.45
A 2.28 2.28 0.0 0 0 35.93 65.73
L 35.93 69.45
A 2.50 2.50 0.0 0 0 38.43 71.95
L 58.97 71.95
A 0.61 0.61 0.0 0 1 59.58 72.56
Q 59.58 75.28 59.60 98.24
C 59.60 101.10 61.65 101.46 64.04 101.46"
/>
</g>
<path fill="#1fc76f" d="
M 115.34 95.08
A 19.82 19.82 0.0 0 1 95.52 114.90
L 32.48 114.90
A 19.82 19.82 0.0 0 1 12.66 95.08
L 12.66 32.88
A 19.82 19.82 0.0 0 1 32.48 13.06
L 95.52 13.06
A 19.82 19.82 0.0 0 1 115.34 32.88
L 115.34 95.08
Z
M 64.04 34.07
Q 64.93 34.07 65.55 34.69
Q 74.55 43.60 82.91 51.96
C 87.46 56.52 91.92 55.95 98.45 55.81
A 2.52 2.52 0.0 0 0 100.92 53.29
L 100.92 49.89
A 2.56 2.55 -1.5 0 0 98.23 47.34
C 95.84 47.46 91.15 47.62 89.22 45.67
Q 81.61 37.99 68.36 24.99
Q 66.43 23.09 64.04 23.09
Q 61.66 23.09 59.72 24.99
Q 46.47 37.98 38.85 45.66
C 36.92 47.61 32.23 47.45 29.84 47.32
A 2.56 2.55 1.5 0 0 27.15 49.87
L 27.15 53.27
A 2.52 2.52 0.0 0 0 29.62 55.79
C 36.15 55.94 40.61 56.51 45.16 51.95
Q 53.53 43.60 62.53 34.69
Q 63.15 34.07 64.04 34.07
Z
M 64.04 101.46
C 66.42 101.46 68.47 101.10 68.47 98.24
Q 68.49 75.28 68.49 72.56
A 0.61 0.61 0.0 0 1 69.10 71.95
L 89.64 71.95
A 2.50 2.50 0.0 0 0 92.14 69.45
L 92.14 65.73
A 2.28 2.28 0.0 0 0 89.86 63.45
L 69.10 63.45
A 0.61 0.61 0.0 0 1 68.49 62.84
L 68.49 50.37
A 3.05 3.03 2.1 0 0 65.67 47.34
Q 65.14 47.31 64.03 47.31
Q 62.93 47.31 62.40 47.34
A 3.05 3.03 -2.1 0 0 59.58 50.37
L 59.58 62.84
A 0.61 0.61 0.0 0 1 58.97 63.45
L 38.21 63.45
A 2.28 2.28 0.0 0 0 35.93 65.73
L 35.93 69.45
A 2.50 2.50 0.0 0 0 38.43 71.95
L 58.97 71.95
A 0.61 0.61 0.0 0 1 59.58 72.56
Q 59.58 75.28 59.60 98.24
C 59.60 101.10 61.65 101.46 64.04 101.46
Z"
/>
<path fill="#fffef2" d="
M 64.04 23.09
Q 66.43 23.09 68.36 24.99
Q 81.61 37.99 89.22 45.67
C 91.15 47.62 95.84 47.46 98.23 47.34
A 2.56 2.55 -1.5 0 1 100.92 49.89
L 100.92 53.29
A 2.52 2.52 0.0 0 1 98.45 55.81
C 91.92 55.95 87.46 56.52 82.91 51.96
Q 74.55 43.60 65.55 34.69
Q 64.93 34.07 64.04 34.07
Q 63.15 34.07 62.53 34.69
Q 53.53 43.60 45.16 51.95
C 40.61 56.51 36.15 55.94 29.62 55.79
A 2.52 2.52 0.0 0 1 27.15 53.27
L 27.15 49.87
A 2.56 2.55 1.5 0 1 29.84 47.32
C 32.23 47.45 36.92 47.61 38.85 45.66
Q 46.47 37.98 59.72 24.99
Q 61.66 23.09 64.04 23.09
Z"
/>
<path fill="#fffef2" d="
M 64.03 47.31
Q 65.14 47.31 65.67 47.34
A 3.05 3.03 2.1 0 1 68.49 50.37
L 68.49 62.84
A 0.61 0.61 0.0 0 0 69.10 63.45
L 89.86 63.45
A 2.28 2.28 0.0 0 1 92.14 65.73
L 92.14 69.45
A 2.50 2.50 0.0 0 1 89.64 71.95
L 69.10 71.95
A 0.61 0.61 0.0 0 0 68.49 72.56
Q 68.49 75.28 68.47 98.24
C 68.47 101.10 66.42 101.46 64.04 101.46
C 61.65 101.46 59.60 101.10 59.60 98.24
Q 59.58 75.28 59.58 72.56
A 0.61 0.61 0.0 0 0 58.97 71.95
L 38.43 71.95
A 2.50 2.50 0.0 0 1 35.93 69.45
L 35.93 65.73
A 2.28 2.28 0.0 0 1 38.21 63.45
L 58.97 63.45
A 0.61 0.61 0.0 0 0 59.58 62.84
L 59.58 50.37
A 3.05 3.03 -2.1 0 1 62.40 47.34
Q 62.93 47.31 64.03 47.31
Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -1,12 +0,0 @@
<svg width="847" height="180" viewBox="0 0 847 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M223.833 67.1063C226.646 56.5688 225.484 48.5063 220.345 42.9188C215.207 37.3313 206.224 34.5188 193.398 34.4813H144.247L114.617 145.65H138.414L150.397 100.65H166.112C168.987 100.506 171.852 101.093 174.438 102.356C175.488 103.013 176.363 103.913 176.99 104.981C177.617 106.048 177.977 107.251 178.039 108.488L182.333 145.65H207.837L203.674 111.131C202.849 103.425 199.305 98.8876 193.154 97.5189C200.596 95.4659 207.387 91.5412 212.882 86.1189C218.191 80.893 221.977 74.3204 223.833 67.1063ZM201.049 67.5001C200.023 72.0579 197.231 76.0227 193.285 78.5251C189.534 80.8876 184.059 82.0501 177.101 82.0501H155.011L162.774 53.0063H184.884C191.822 53.0063 196.586 54.1876 199.211 56.5313C201.836 58.8751 202.399 62.5876 201.049 67.5001Z" fill="#0C2651"/>
<path d="M292.544 66.1875L289.524 77.4375C288.266 73.4169 285.604 69.9828 282.023 67.7625C277.847 65.2563 273.032 64.0185 268.165 64.2C261.337 64.2018 254.627 65.9856 248.7 69.375C242.248 73.0744 236.676 78.1279 232.366 84.1875C227.616 90.7391 224.137 98.1228 222.108 105.956C219.98 112.928 219.53 120.303 220.795 127.481C221.761 133.151 224.795 138.261 229.309 141.825C234.216 145.333 240.161 147.09 246.187 146.813C251.568 146.825 256.888 145.673 261.782 143.437C266.676 141.2 271.028 137.932 274.541 133.856L271.428 145.594H294.625L315.778 66.1875H292.544ZM282.005 105.563C280.57 112.028 277.377 117.972 272.778 122.738C270.798 124.676 268.451 126.201 265.876 127.225C263.3 128.248 260.547 128.75 257.776 128.7C251.869 128.7 247.893 126.825 245.755 122.831C243.618 118.838 243.561 113.269 245.512 105.956C246.995 99.433 250.238 93.441 254.888 88.6313C256.875 86.6345 259.24 85.0539 261.845 83.9819C264.45 82.9098 267.242 82.3678 270.059 82.3875C275.835 82.3875 279.773 84.3938 281.798 88.3875C283.824 92.3813 284.03 98.025 282.042 105.544L282.005 105.563Z" fill="#0C2651"/>
<path d="M398.534 66.2437H331.718L326.786 84.7687H365.679L314.54 129.375L310.171 145.744H379.181L384.113 127.219H342.388L394.371 81.9749L398.534 66.2437Z" fill="#0C2651"/>
<path d="M471.614 69.1501C465.409 65.605 458.332 63.8766 451.192 64.1626C443.138 64.0929 435.167 65.7956 427.845 69.1501C420.723 72.4705 414.465 77.3925 409.561 83.5314C404.438 89.9708 400.738 97.4217 398.703 105.394C396.514 112.484 396.243 120.027 397.915 127.256C399.45 133.257 403.264 138.423 408.548 141.656C413.924 144.969 420.8 146.625 429.176 146.625C437.144 146.695 445.028 144.998 452.261 141.656C459.361 138.335 465.589 133.405 470.451 127.256C475.584 120.821 479.291 113.369 481.328 105.394C483.521 98.3039 483.786 90.7579 482.097 83.5314C480.62 77.5504 476.856 72.387 471.614 69.1501ZM458.074 105.356C455.993 113.156 452.936 119.006 448.81 122.869C444.829 126.661 439.512 128.73 434.014 128.625C421.888 128.625 417.893 120.869 422.031 105.356C424.069 97.6564 427.195 91.8501 431.408 87.9376C435.44 84.0812 440.831 81.9721 446.41 82.0689C452.186 82.0689 456.124 83.9439 458.149 87.9376C460.175 91.9314 460.118 97.6501 458.074 105.356Z" fill="#0C2651"/>
<path d="M724.213 66.1875L721.194 77.4375C719.936 73.4169 717.274 69.9828 713.693 67.7625C709.524 65.2336 704.708 63.9759 699.835 64.1438C692.995 64.1586 686.278 65.962 680.351 69.375C673.894 73.0736 668.315 78.1271 663.998 84.1875C659.256 90.7434 655.777 98.1257 653.74 105.956C651.631 112.931 651.181 120.302 652.428 127.481C653.394 133.151 656.427 138.261 660.942 141.825C665.913 145.362 671.932 147.119 678.025 146.813C683.286 146.793 688.483 145.655 693.271 143.475C698.212 141.258 702.599 137.973 706.117 133.856L703.004 145.594H726.201L747.373 66.1875H724.213ZM713.618 105.563C712.19 112.026 709.004 117.97 704.41 122.738C702.426 124.67 700.079 126.191 697.504 127.215C694.93 128.238 692.178 128.743 689.408 128.7C683.482 128.7 679.507 126.825 677.369 122.831C675.231 118.838 675.194 113.269 677.125 105.956C678.616 99.4361 681.858 93.4461 686.502 88.6313C688.493 86.6357 690.862 85.056 693.469 83.9841C696.077 82.9122 698.872 82.3695 701.691 82.3875C707.467 82.3875 711.38 84.3875 713.431 88.3875C715.606 92.2688 715.662 98.025 713.618 105.544V105.563Z" fill="#0C2651"/>
<path d="M554.182 87.5628L560.108 65.9441C557.609 64.8129 554.88 64.28 552.138 64.3878C546.778 64.3939 541.509 65.7755 536.836 68.4003C532.574 70.7348 528.875 73.973 525.997 77.8878L529.053 66.2441H505.875L484.534 145.65H508.05L519.096 104.194C520.39 98.6623 523.419 93.6891 527.741 90.0003C532.185 86.5295 537.707 84.7309 543.343 84.9191C547.118 84.9033 550.839 85.8109 554.182 87.5628Z" fill="#0C2651"/>
<path d="M636.132 69.5437C631.334 65.8774 625.4 64.0126 619.367 64.275C613.497 64.2816 607.712 65.6825 602.49 68.3625C597.257 70.9353 592.746 74.7691 589.363 79.5187L589.457 78.975L593.413 66.2437H570.348L564.459 88.3125C564.459 88.5562 564.328 88.8187 564.272 89.0625L540.081 180H563.709L575.917 134.175C577.084 138.218 579.773 141.652 583.418 143.756C587.649 146.186 592.476 147.382 597.352 147.206C604.201 147.225 610.946 145.523 616.967 142.256C623.375 138.725 628.906 133.797 633.151 127.837C637.82 121.319 641.235 113.987 643.221 106.219C645.364 99.143 645.852 91.6692 644.646 84.375C643.659 78.5774 640.641 73.3201 636.132 69.5437ZM619.424 105.844C617.982 112.207 614.809 118.047 610.253 122.719C606.222 126.574 600.829 128.677 595.251 128.569C589.438 128.569 585.5 126.562 583.418 122.606C581.337 118.65 581.224 112.95 583.268 105.431C584.729 98.9159 587.976 92.9362 592.645 88.1625C594.628 86.2258 596.974 84.699 599.548 83.6695C602.123 82.64 604.875 82.1279 607.647 82.1625C613.273 82.1625 617.192 84.2625 619.236 88.4062C621.28 92.55 621.411 98.4187 619.424 105.844Z" fill="#0C2651"/>
<path d="M846.013 63.5999H821.634L817.884 71.4374C817.584 71.8311 817.302 72.2249 816.965 72.7124L816.571 73.3499L786.566 115.312L780.341 66.2436H755.737L768.245 141.506L740.585 180H764.269L771.114 170.231C771.321 169.931 771.527 169.687 771.714 169.369L779.722 157.95L779.984 157.65L815.746 106.631L846.013 63.5999Z" fill="#0C2651"/>
<path d="M54.3828 47.1751L47.2567 73.1251L87.4627 47.0438L61.2088 145.65H87.8565L126.656 6.10352e-05L54.3828 47.1751Z" fill="#3395FF"/>
<path d="M11.0641 104.175L0 145.65H54.6643L77.0364 61.2748L11.0641 104.175Z" fill="#0C2651"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 468 222.5" style="enable-background:new 0 0 468 222.5;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#635BFF;}
</style>
<g>
<path class="st0" d="M414,113.4c0-25.6-12.4-45.8-36.1-45.8c-23.8,0-38.2,20.2-38.2,45.6c0,30.1,17,45.3,41.4,45.3
c11.9,0,20.9-2.7,27.7-6.5v-20c-6.8,3.4-14.6,5.5-24.5,5.5c-9.7,0-18.3-3.4-19.4-15.2h48.9C413.8,121,414,115.8,414,113.4z
M364.6,103.9c0-11.3,6.9-16,13.2-16c6.1,0,12.6,4.7,12.6,16H364.6z"/>
<path class="st0" d="M301.1,67.6c-9.8,0-16.1,4.6-19.6,7.8l-1.3-6.2h-22v116.6l25-5.3l0.1-28.3c3.6,2.6,8.9,6.3,17.7,6.3
c17.9,0,34.2-14.4,34.2-46.1C335.1,83.4,318.6,67.6,301.1,67.6z M295.1,136.5c-5.9,0-9.4-2.1-11.8-4.7l-0.1-37.1
c2.6-2.9,6.2-4.9,11.9-4.9c9.1,0,15.4,10.2,15.4,23.3C310.5,126.5,304.3,136.5,295.1,136.5z"/>
<polygon class="st0" points="223.8,61.7 248.9,56.3 248.9,36 223.8,41.3 "/>
<rect x="223.8" y="69.3" class="st0" width="25.1" height="87.5"/>
<path class="st0" d="M196.9,76.7l-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7,15.9-6.3,19-5.2v-23C214.5,68.1,202.8,65.9,196.9,76.7z"/>
<path class="st0" d="M146.9,47.6l-24.4,5.2l-0.1,80.1c0,14.8,11.1,25.7,25.9,25.7c8.2,0,14.2-1.5,17.5-3.3V135
c-3.2,1.3-19,5.9-19-8.9V90.6h19V69.3h-19L146.9,47.6z"/>
<path class="st0" d="M79.3,94.7c0-3.9,3.2-5.4,8.5-5.4c7.6,0,17.2,2.3,24.8,6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6
C67.5,67.6,54,78.2,54,95.9c0,27.6,38,23.2,38,35.1c0,4.6-4,6.1-9.6,6.1c-8.3,0-18.9-3.4-27.3-8v23.8c9.3,4,18.7,5.7,27.3,5.7
c20.8,0,35.1-10.3,35.1-28.2C117.4,100.6,79.3,105.9,79.3,94.7z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,34 +0,0 @@
@import 'jingrow-ui/src/style.css';
@layer components {
/* Works on Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #c0c6cc #ebeef0;
}
html {
scrollbar-width: auto;
}
/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar-thumb {
background: #c0c6cc;
border-radius: 6px;
}
*::-webkit-scrollbar-track,
*::-webkit-scrollbar-corner {
background: #ebeef0;
}
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
body::-webkit-scrollbar {
width: 12px;
height: 12px;
}
}

View File

@ -1,159 +0,0 @@
<template>
<Alert :title="alertTitle" v-if="show">
<span v-if="deployInformation.deploy_in_progress"
>A deploy for this bench is in progress</span
>
<span v-else-if="bench.status == 'Active'">
A new update is available for your bench. Would you like to deploy the
update now?
</span>
<span v-else>
Your bench is not deployed yet. You can add more apps to your bench before
deploying. If you want to deploy now, click on Deploy.
</span>
<template #actions>
<Button
v-if="deployInformation.deploy_in_progress"
variant="solid"
:route="`/groups/${bench.name}/deploys/${deployInformation.last_deploy.name}`"
>View Progress</Button
>
<Tooltip
v-else
:text="
!permissions.update
? `You don't have enough permissions to perform this action`
: ''
"
>
<Button
variant="solid"
:disabled="!permissions.update"
@click="showDeployDialog = true"
>
Show updates
</Button>
</Tooltip>
</template>
<Dialog
:options="{ title: 'Select the apps you want to update' }"
v-model="showDeployDialog"
>
<template v-slot:body-content>
<BenchAppUpdates
:apps="deployInformation.apps"
v-model:selectedApps="selectedApps"
:removedApps="deployInformation.removed_apps"
/>
<ErrorMessage class="mt-2" :message="errorMessage" />
</template>
<template v-slot:actions>
<Button
class="w-full"
variant="solid"
@click="$resources.deploy.submit()"
:loading="$resources.deploy.loading"
v-if="this.bench.team === $account.team.name"
>
Deploy
</Button>
<Button
class="w-full"
variant="solid"
@click="showTeamSwitcher = true"
v-else
>
Switch Team
</Button>
<SwitchTeamDialog v-model="showTeamSwitcher" />
</template>
</Dialog>
</Alert>
</template>
<script>
import BenchAppUpdates from './BenchAppUpdates.vue';
import SwitchTeamDialog from './SwitchTeamDialog.vue';
export default {
name: 'AlertBenchUpdate',
props: ['bench'],
components: {
BenchAppUpdates,
SwitchTeamDialog
},
data() {
return {
showDeployDialog: false,
showTeamSwitcher: false,
selectedApps: []
};
},
resources: {
deployInformation() {
return {
url: 'jcloud.api.bench.deploy_information',
params: {
name: this.bench?.name
},
auto: true
};
},
deploy() {
return {
url: 'jcloud.api.bench.deploy',
params: {
name: this.bench?.name,
apps: this.selectedApps
},
validate() {
if (
this.selectedApps.length === 0 &&
this.deployInformation.removed_apps.length === 0
) {
return 'You must select atleast 1 app to proceed with update.';
}
},
onSuccess(candidate) {
this.$router.push(`/groups/${this.bench.name}/deploys/${candidate}`);
this.showDeployDialog = false;
}
};
}
},
computed: {
permissions() {
return {
update: this.$account.hasPermission(
this.bench.name,
'jcloud.api.bench.deploy_and_update'
)
};
},
show() {
if (this.deployInformation) {
return (
this.deployInformation.update_available &&
['Awaiting Deploy', 'Active'].includes(this.bench.status)
);
}
},
errorMessage() {
return (
this.$resources.deploy.error ||
(this.bench.team !== $account.team.name
? "Current Team doesn't have enough permissions"
: '')
);
},
deployInformation() {
return this.$resources.deployInformation.data;
},
alertTitle() {
if (this.deployInformation && this.deployInformation.deploy_in_progress) {
return 'Deploy in Progress';
}
return this.bench.status == 'Active' ? 'Update Available' : 'Deploy';
}
}
};
</script>

View File

@ -1,65 +0,0 @@
<template>
<Alert title="Account Setup" v-if="!$account.hasBillingInfo">
{{ message }}
<template #actions>
<Button
variant="solid"
@click="
isDefaultPaymentModeCard
? (showPrepaidCreditsDialog = true)
: (showCardDialog = true)
"
class="whitespace-nowrap"
>
{{
isDefaultPaymentModeCard ? 'Add Balance' : 'Add Billing Information'
}}
</Button>
</template>
<BillingInformationDialog v-model="showCardDialog" v-if="showCardDialog" />
<PrepaidCreditsDialog
v-if="showPrepaidCreditsDialog"
v-model:show="showPrepaidCreditsDialog"
:minimum-amount="$account.team.currency === 'CNY' ? 0.01 : 0.01"
@success="handleAddPrepaidCreditsSuccess"
/>
</Alert>
</template>
<script>
import { defineAsyncComponent } from 'vue';
export default {
name: 'AlertBillingInformation',
components: {
BillingInformationDialog: defineAsyncComponent(() =>
import('./BillingInformationDialog.vue')
),
PrepaidCreditsDialog: defineAsyncComponent(() =>
import('./PrepaidCreditsDialog.vue')
)
},
data() {
return {
showCardDialog: false,
showPrepaidCreditsDialog: false
};
},
methods: {
handleAddPrepaidCreditsSuccess() {
this.showPrepaidCreditsDialog = false;
}
},
computed: {
isDefaultPaymentModeCard() {
return this.$account.team.payment_mode == 'Card';
},
message() {
if (this.isDefaultPaymentModeCard) {
return "We couldn't verify your card with micro charge. Please add some balance to your account to start creating sites.";
} else {
return "You haven't added your billing information yet. Add it to start creating sites.";
}
}
}
};
</script>

View File

@ -1,32 +0,0 @@
<template>
<Alert
title="Site Activation"
v-if="site.status == 'Active' && !site.setup_wizard_complete"
>
<span>
Please login and complete the setup wizard on your site. Analytics will be
collected only after setup is complete.
</span>
<template #actions>
<Button
variant="solid"
@click="$resources.loginAsAdmin.submit()"
:loading="$resources.loginAsAdmin.loading"
>
Login
</Button>
</template>
</Alert>
</template>
<script>
import { loginAsAdmin } from '@/controllers/loginAsAdmin';
export default {
name: 'AlertSiteActivation',
props: ['site'],
resources: {
loginAsAdmin() {
return loginAsAdmin(this.site?.name);
}
}
};
</script>

View File

@ -1,165 +0,0 @@
<template>
<Alert title="Update Available" v-if="show">
<span>
A new update is available for your site. Would you like to update your
site now?
</span>
<template #actions>
<Tooltip
:text="
!permissions.update
? `You don't have enough permissions to perform this action`
: ''
"
>
<Button
:disabled="!permissions.update"
variant="solid"
@click="showUpdatesDialog = true"
>
Show updates
</Button>
</Tooltip>
</template>
<Dialog
:options="{
title: 'Updates available',
actions: [
{
label: 'Update Now',
variant: 'solid',
onClick: () => $resources.scheduleUpdate.fetch()
}
]
}"
v-model="showUpdatesDialog"
>
<template v-slot:body-content>
<SiteAppUpdates :apps="updateAvailableApps" />
<div class="mt-4" v-if="updateAvailableApps.length">
<!-- Skip Failing Checkbox -->
<input
id="skip-failing"
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
v-model="wantToSkipFailingPatches"
/>
<label for="skip-failing" class="ml-1 text-sm text-gray-900">
Skip failing patches if any?
</label>
</div>
<div class="mt-2" v-if="skip_backups">
<!-- Skip Site Backup -->
<input
id="skip-backup"
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
v-model="wantToSkipBackups"
/>
<label for="skip-backup" class="ml-1 text-sm text-gray-900">
Update without site backup?
</label>
<div class="mt-1 text-sm text-red-600" v-if="wantToSkipBackups">
In case of failure, you won't be able to restore the site.
</div>
</div>
<ErrorMessage class="mt-1" :message="$resources.scheduleUpdate.error" />
</template>
</Dialog>
</Alert>
</template>
<script>
import SiteAppUpdates from './SiteAppUpdates.vue';
import { notify } from '@/utils/toast';
export default {
name: 'AlertSiteUpdate',
props: ['site'],
components: {
SiteAppUpdates
},
data() {
return {
showUpdatesDialog: false,
wantToSkipFailingPatches: false,
wantToSkipBackups: false
};
},
resources: {
updateInformation() {
return {
url: 'jcloud.api.site.check_for_updates',
params: {
name: this.site?.name
},
auto: true
};
},
lastMigrateFailed() {
return {
url: 'jcloud.api.site.last_migrate_failed',
params: {
name: this.site?.name
},
auto: true
};
},
scheduleUpdate() {
return {
url: 'jcloud.api.site.update',
params: {
name: this.site?.name,
skip_failing_patches: this.wantToSkipFailingPatches,
skip_backups: this.wantToSkipBackups
},
onSuccess() {
this.showUpdatesDialog = false;
notify({
title: 'Site update scheduled successfully',
icon: 'check',
color: 'green'
});
}
};
}
},
computed: {
permissions() {
return {
update: this.$account.hasPermission(
this.site.name,
'jcloud.api.site.update'
)
};
},
show() {
if (this.updateInformation) {
return (
this.site.setup_wizard_complete &&
this.updateInformation.update_available &&
['Active', 'Inactive', 'Suspended', 'Broken'].includes(
this.site.status
)
);
}
},
updateInformation() {
return this.$resources.updateInformation.data;
},
updateAvailableApps() {
const installedApps = this.updateInformation.installed_apps;
const updateAvailableApps = this.updateInformation.apps;
return updateAvailableApps.filter(app =>
installedApps.find(installedApp => installedApp.app === app.app)
);
},
lastMigrateFailed() {
return this.$resources.lastMigrateFailed.data;
},
skip_backups() {
return this.$account.team?.skip_backups;
}
}
};
</script>

View File

@ -1,172 +0,0 @@
<template>
<Alert :title="alertTitle" v-if="show">
<span v-if="deployInformation.deploy_in_progress"
>A deploy for this bench is in progress</span
>
<span v-else-if="bench.status == 'Active'">
A new update is available for your bench. Would you like to deploy the
update now?
</span>
<span v-else>
Your bench is not deployed yet. You can add more apps to your bench before
deploying. If you want to deploy now, click on the Show Updates button.
</span>
<template #actions>
<Button
v-if="deployInformation.deploy_in_progress"
variant="solid"
:route="`/groups/${bench.name}/deploys/${deployInformation.last_deploy.name}`"
>View Progress</Button
>
<Button
v-else
variant="solid"
@click="
() => {
showDeployDialog = true;
step = 'Apps';
}
"
>
Show Updates
</Button>
</template>
<Dialog
:options="{
title:
step == 'Apps'
? 'Select the apps you want to update'
: 'Select the sites you want to update'
}"
v-model="showDeployDialog"
>
<template v-slot:body-content>
<BenchAppUpdates
v-if="step == 'Apps'"
:apps="deployInformation.apps"
v-model:selectedApps="selectedApps"
:removedApps="deployInformation.removed_apps"
/>
<BenchSiteUpdates
class="p-1"
v-if="step == 'Sites'"
:sites="deployInformation.sites"
v-model:selectedSites="selectedSites"
/>
<ErrorMessage class="mt-2" :message="errorMessage" />
</template>
<template v-slot:actions>
<Button v-if="step == 'Sites'" class="w-full" @click="step = 'Apps'">
Back
</Button>
<Button
v-if="step == 'Sites'"
variant="solid"
class="mt-2 w-full"
@click="$resources.deploy.submit()"
:loading="$resources.deploy.loading"
>
{{ selectedSites.length > 0 ? 'Update' : 'Skip and Deploy' }}
</Button>
<Button v-else variant="solid" class="w-full" @click="step = 'Sites'">
Next
</Button>
</template>
</Dialog>
</Alert>
</template>
<script>
import BenchAppUpdates from './BenchAppUpdates.vue';
import BenchSiteUpdates from './BenchSiteUpdates.vue';
import SwitchTeamDialog from './SwitchTeamDialog.vue';
import { notify } from '@/utils/toast';
export default {
name: 'AlertBenchUpdate',
props: ['bench'],
components: {
BenchAppUpdates,
BenchSiteUpdates,
SwitchTeamDialog
},
data() {
return {
showDeployDialog: false,
showTeamSwitcher: false,
selectedApps: [],
selectedSites: [],
step: 'Apps'
};
},
resources: {
deployInformation() {
return {
url: 'jcloud.api.bench.deploy_information',
params: {
name: this.bench?.name
},
auto: true
};
},
deploy() {
return {
url: 'jcloud.api.bench.deploy_and_update',
params: {
name: this.bench?.name,
apps: this.selectedApps,
sites: this.selectedSites
},
validate() {
if (
this.selectedApps.length === 0 &&
this.deployInformation.removed_apps.length === 0
) {
return 'You must select atleast 1 app to proceed with update.';
}
},
onSuccess(new_candidate_name) {
this.showDeployDialog = false;
this.$resources.deployInformation.setData({
...this.$resources.deployInformation.data,
deploy_in_progress: true,
last_deploy: { name: new_candidate_name, status: 'Running' }
});
notify({
title: 'Updates scheduled successfully',
icon: 'check',
color: 'green'
});
}
};
}
},
computed: {
show() {
if (this.deployInformation) {
return (
this.deployInformation.update_available &&
['Awaiting Deploy', 'Active'].includes(this.bench.status)
);
}
},
errorMessage() {
return (
this.$resources.deploy.error ||
(this.bench.team !== $account.team.name
? "Current Team doesn't have enough permissions"
: '')
);
},
deployInformation() {
return this.$resources.deployInformation.data;
},
alertTitle() {
if (this.deployInformation && this.deployInformation.deploy_in_progress) {
return 'Deploy in Progress';
}
return this.bench.status == 'Active' ? 'Update Available' : 'Deploy';
}
}
};
</script>

View File

@ -1,98 +0,0 @@
<template>
<div
v-if="plan"
class="flex flex-col justify-between rounded-2xl border border-gray-100 p-5 shadow"
:class="[
popular ? 'relative bg-blue-100' : '',
selected ? 'relative ring-2 ring-inset ring-blue-500' : '',
clickable ? 'cursor-pointer hover:border-gray-300' : ''
]"
>
<div>
<div
v-if="popular"
class="absolute -top-3 left-1/4 rounded-md bg-blue-500 py-1 px-2 text-center text-xs"
>
<h5 class="font-medium uppercase text-white">Most Popular</h5>
</div>
<input
v-if="selected"
type="checkbox"
class="absolute top-3 right-3 h-4 w-4 rounded border-gray-300 text-blue-500"
checked
disabled
/>
<h4 class="flex justify-between text-xl font-semibold text-gray-900">
<div>
<span v-if="plan.price_usd <= 0"> Free </span>
<span v-else>
{{ $planTitle(plan) }}
<span class="text-base font-normal text-gray-600">
{{ plan.block_monthly === 1 ? '/year' : '/mo' }}
</span>
</span>
</div>
<div v-if="editable">
<Button icon-left="edit" @click="e => $emit('beginEdit', e)"
>Edit</Button
>
</div>
</h4>
<!--<h4
v-if="plan.discounted"
class="mt-1 text-base text-gray-600 line-through"
>
{{
$planTitle({
price_usd: plan.price_usd_before_discount,
price_cny: plan.price_cny_before_discount
})
}}
</h4>-->
<FeatureList class="mt-5" :features="plan.features" />
</div>
<Badge
v-if="editable"
:label="plan.enabled ? 'Enabled' : 'Disabled'"
class="mt-4 self-start"
></Badge>
</div>
</template>
<script>
import FeatureList from '@/components/FeatureList.vue';
export default {
name: 'AppPlanCard',
emits: ['beginEdit'],
props: {
plan: {
type: Object
},
popular: {
type: Boolean,
default: false
},
selected: {
type: Boolean,
default: false
},
clickable: {
type: Boolean,
default: true
},
editable: {
type: Boolean,
default: false
}
},
components: {
FeatureList
}
};
</script>

View File

@ -1,110 +0,0 @@
<template>
<div class="space-y-3 max-h-96 overflow-auto px-4">
<button
class="block w-full cursor-pointer rounded-md border px-4 py-3 text-left shadow ring-inset focus:outline-none"
:class="
isAppSelected(app)
? 'bg-gray-50 ring-2 ring-gray-600'
: 'cursor-pointer hover:border-gray-300'
"
v-for="app in apps"
:key="app.name"
@click="toggleApp(app.name)"
>
<div class="ml-1 flex items-center justify-between text-left text-base">
<div>
<div class="font-semibold">
{{ app.title }}
</div>
<div class="text-gray-700">
{{ app.source.repository_owner }}/{{ app.source.repository }}
</div>
</div>
<Dropdown :options="dropdownItems(app)" right>
<template v-slot="{ open }">
<Button type="white" icon-right="chevron-down">
<span>{{ app.source.branch }}</span>
</Button>
</template>
</Dropdown>
</div>
</button>
</div>
</template>
<script>
export default {
name: 'AppSourceSelector',
props: ['apps', 'modelValue', 'multiple'],
emits: ['update:modelValue'],
methods: {
toggleApp(appName) {
let mapApp = app => ({ app: app.name, source: app.source });
if (!this.multiple) {
let selectedApp = this.apps.find(app => app.name === appName);
this.$emit('update:modelValue', mapApp(selectedApp));
return;
}
// multiple
let selectedAppsMap = Object.assign({}, this.selectedAppsMap);
if (selectedAppsMap[appName]) {
// exists already, remove
selectedAppsMap[appName] = false;
} else {
// add
selectedAppsMap[appName] = true;
}
let selectedApps = this.apps
.filter(app => selectedAppsMap[app.name])
.map(mapApp);
this.$emit('update:modelValue', selectedApps);
},
isAppSelected(app) {
if (this.multiple) {
return this.selectedAppsMap[app.name];
}
return this.modelValue && this.modelValue.app === app.name;
},
dropdownItems(app) {
return app.sources.map(source => ({
label: `${source.repository_owner}/${source.repository}:${source.branch}`,
onClick: () => this.selectSource(app, source)
}));
},
selectSource(app, source) {
app.source = source;
if (this.multiple) {
let selectedApps = this.modelValue.map(_app => {
if (app.name === _app.app) {
return {
app: app.name,
source
};
}
return _app;
});
this.$emit('update:modelValue', selectedApps);
} else {
this.$emit('update:modelValue', {
app: app.name,
source
});
}
}
},
computed: {
selectedAppsMap() {
if (!this.multiple) return {};
let out = {};
let selectedAppNames = this.modelValue.map(app => app.app);
for (let app of this.apps) {
out[app.name] = selectedAppNames.includes(app.name);
}
return out;
}
}
};
</script>

View File

@ -1,75 +0,0 @@
<template>
<div>
<h3>All apps (3)</h3>
<ul class="mt-5 divide-y divide-gray-300">
<li class="flex flex-row items-start justify-between pb-3">
<div class="flex flex-row self-end">
<!-- Replace with app icon -->
<div class="mr-3 h-10 w-10 self-center rounded-lg bg-red-400"></div>
<div class="flex flex-col">
<h4 class="text-lg font-medium text-gray-900">Jingrow Mail</h4>
<p class="mt-1 text-base text-gray-600">Best open source ERP.</p>
</div>
</div>
<div class="flex flex-col">
<div
class="rounded-md bg-blue-100 py-1 px-2 text-xs uppercase text-blue-500"
>
Most popular
</div>
<p
class="mt-1 self-end text-right text-xl font-semibold text-gray-900"
>
$10<span class="text-base font-normal text-gray-600">/ Month</span>
</p>
</div>
</li>
<li class="flex flex-row items-center justify-between py-3">
<div class="flex flex-row self-end">
<!-- Replace with app icon -->
<div class="mr-3 h-10 w-10 self-center rounded-lg bg-green-400"></div>
<div class="flex flex-col">
<h4 class="text-lg font-medium text-gray-900">Darkify</h4>
<p class="mt-1 text-base text-gray-600">Best open source ERP.</p>
</div>
</div>
<div class="flex flex-col">
<p
class="mt-1 self-center text-right text-xl font-semibold text-gray-900"
>
$49<span class="text-base font-normal text-gray-600">/ Month</span>
</p>
</div>
</li>
<li class="flex flex-row items-center justify-between py-3">
<div class="flex flex-row self-end">
<!-- Replace with app icon -->
<div
class="mr-3 h-10 w-10 self-center rounded-lg bg-indigo-400"
></div>
<div class="flex flex-col">
<h4 class="text-lg font-medium text-gray-900">Jingrow Healthcare</h4>
<p class="mt-1 text-base text-gray-600">Best open source ERP.</p>
</div>
</div>
<div class="flex flex-col">
<p
class="mt-1 self-end text-right text-xl font-semibold text-gray-900"
>
$129<span class="text-base font-normal text-gray-600">/ Month</span>
</p>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'AppSubscriptionSummary',
data() {
return {};
}
};
</script>

View File

@ -1,121 +0,0 @@
<template>
<button
class="flex w-full flex-row items-center justify-between rounded-lg border border-gray-100 px-4 py-2 shadow focus:outline-none"
:class="[
selected || uninstall ? 'ring-2 ring-inset ring-gray-600' : '',
selectable ? 'hover:border-gray-300' : 'cursor-default'
]"
ref="card"
>
<div class="flex flex-row items-center gap-2">
<input
v-if="selectable"
@click.self="$refs['card'].click()"
:checked="selected"
type="checkbox"
class="h-4 w-4 cursor-pointer rounded border-gray-300 text-gray-600 focus:ring-transparent"
/>
<h3 class="text-left text-lg font-medium text-gray-900">
{{ app.title }}
</h3>
</div>
<Badge
v-if="uninstall"
class="my-1"
theme="red"
label="Will Be Uninstalled "
/>
<div v-else class="ml-2 flex flex-row items-center space-x-2">
<CommitTag
v-if="deployFrom"
:tag="deployFrom"
:link="`${app.repository_url}/commit/${app.current_hash}`"
/>
<a
v-if="deployFrom"
class="flex cursor-pointer flex-col justify-center"
:href="`${app.repository_url}/compare/${app.current_hash}...${getHash(
deployTo.value
)}`"
target="_blank"
>
<FeatherIcon name="arrow-right" class="w-4" />
</a>
<Badge
v-else
label="First Deploy"
theme="green"
class="whitespace-nowrap"
/>
<CommitChooser :options="autocompleteOptions" v-model="deployTo" />
</div>
</button>
</template>
<script>
import CommitChooser from './utils/CommitChooser.vue';
import CommitTag from './utils/CommitTag.vue';
export default {
name: 'AppUpdateCard',
props: ['app', 'selectable', 'selected', 'uninstall'],
data() {
return {
deployTo: {
label: this.initialDeployTo(),
value: this.app.next_release
}
};
},
watch: {
deployTo(newVal) {
this.app.next_release = newVal.value;
this.$emit('update:app', this.app);
}
},
computed: {
deployFrom() {
if (this.app.will_branch_change) {
return this.app.current_branch;
}
return this.app.current_hash
? this.app.current_tag || this.app.current_hash.slice(0, 7)
: null;
},
autocompleteOptions() {
return this.app.releases.map(release => {
const messageMaxLength = 75;
let message = release.message.split('\n')[0];
message =
message.length > messageMaxLength
? message.slice(0, messageMaxLength) + '...'
: message;
return {
label: release.tag
? release.tag
: `${message} (${release.hash.slice(0, 7)})`,
value: release.name
};
});
}
},
methods: {
initialDeployTo() {
if (this.uninstall) return '';
let next_release = this.app.releases.filter(
release => release.name === this.app.next_release
)[0];
if (this.app.will_branch_change) {
return this.app.branch;
} else {
return next_release.tag || next_release.hash.slice(0, 7);
}
},
getHash(tag) {
return this.app.releases.find(release => release.name === tag).hash;
}
},
components: { CommitTag, CommitChooser }
};
</script>

View File

@ -1,32 +0,0 @@
<template>
<div class="mb-4 flex items-center">
<Avatar size="lg" :label="label" :image="image" />
<FileUploader @success="onChange" fileTypes="image/*">
<template v-slot="{ openFileSelector, uploading, progress, error }">
<div class="ml-4">
<Button :loading="uploading" @click="openFileSelector()">
<span v-if="uploading">Uploading {{ progress }}%</span>
<span v-else>{{ label }}</span>
</Button>
<ErrorMessage class="mt-1" :message="error" />
</div>
</template>
</FileUploader>
</div>
</template>
<script>
import FileUploader from '@/components/FileUploader.vue';
export default {
name: 'AvatarUploader',
components: {
FileUploader
},
props: ['image', 'label'],
methods: {
onChange(file) {
this.$emit('update:image', file.file_url);
}
}
};
</script>

View File

@ -1,147 +0,0 @@
<template>
<div>
<div class="mt-2 space-y-2">
<FileUploader
v-for="file in files"
:fileTypes="file.ext"
:key="file.type"
:type="file.type"
@success="onFileUpload(file, $event)"
:fileValidator="f => databaseBackupChecker(f, file.type)"
:s3="true"
>
<template
v-slot="{
file: fileObj,
uploading,
progress,
error,
success,
openFileSelector
}"
>
<ListItem
class="border-b"
:title="fileObj ? fileObj.name : file.title"
>
<template #subtitle>
<span
class="text-base"
:class="error ? 'text-red-500' : 'text-gray-600'"
>
{{
uploading
? `上传中 ${progress}%`
: success
? formatBytes(fileObj.size)
: error
? error
: file.description
}}
</span>
</template>
<template #actions>
<Button
:loading="uploading"
loadingText="上传中..."
@click="openFileSelector()"
v-if="!success"
>
上传
</Button>
<GreenCheckIcon class="w-5" v-if="success" />
</template>
</ListItem>
</template>
</FileUploader>
</div>
</div>
</template>
<script>
import FileUploader from './FileUploader.vue';
export default {
name: 'BackupFilesUploader',
components: { FileUploader },
emits: ['update:backupFiles'],
props: ['backupFiles'],
data() {
return {
files: [
{
icon: '<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.33325 9.33333V22.6667C5.33325 25.6133 10.1093 28 15.9999 28C21.8906 28 26.6666 25.6133 26.6666 22.6667V9.33333M5.33325 9.33333C5.33325 12.28 10.1093 14.6667 15.9999 14.6667C21.8906 14.6667 26.6666 12.28 26.6666 9.33333M5.33325 9.33333C5.33325 6.38667 10.1093 4 15.9999 4C21.8906 4 26.6666 6.38667 26.6666 9.33333M26.6666 16C26.6666 18.9467 21.8906 21.3333 15.9999 21.3333C10.1093 21.3333 5.33325 18.9467 5.33325 16" stroke="#1F272E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
type: 'database',
ext: 'application/x-gzip,application/sql,.sql',
title: '数据库备份',
description:
'上传数据库备份文件。通常文件名以 .sql.gz 或 .sql 结尾',
file: null
},
{
icon: '<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.39111 6.3913H26.3476V22.2174C26.3476 25.9478 23.2955 29 19.565 29H9.39111V6.3913Z" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M13.9131 13.1739H21.8261" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M13.9131 17.6957H21.8261" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M13.9131 22.2173H19.8479" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M22.9565 6.3913V3H6V25.6087H9.3913" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/></svg>',
type: 'public',
ext: 'application/x-tar',
title: '公共文件',
description:
'上传公共文件备份。通常文件名以 -files.tar 结尾',
file: null
},
{
icon: '<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.39111 6.3913H25.3476V22.2174C25.3476 25.9478 22.2955 29 18.565 29H8.39111V6.3913Z" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M21.9565 6.3913V3H5V25.6087H8.3913" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/></svg>',
type: 'private',
ext: 'application/x-tar',
title: '私有文件',
description:
'上传私有文件备份。通常文件名以 -private-files.tar 结尾',
file: null
},
{
icon: '<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.39111 6.3913H25.3476V22.2174C25.3476 25.9478 22.2955 29 18.565 29H8.39111V6.3913Z" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M21.9565 6.3913V3H5V25.6087H8.3913" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/></svg>',
type: 'config',
ext: 'application/json',
title: '站点配置(如备份已加密则必需)',
description:
'上传站点配置文件。通常文件名以 -site_config_backup.json 结尾',
file: null
}
]
};
},
methods: {
onFileUpload(file, data) {
let backupFiles = Object.assign({}, this.backupFiles);
backupFiles[file.type] = data;
this.$emit('update:backupFiles', backupFiles);
},
async databaseBackupChecker(file, type) {
if (type === 'database') {
// valid strings are "database.sql.gz", "database.sql", "database.sql (1).gz", "database.sql (2).gz"
if (!/\.sql( \(\d\))?\.gz$|\.sql$/.test(file.name)) {
throw new Error(
'数据库备份文件应以"database.sql.gz"或"database.sql"结尾'
);
}
if (
![
'application/x-gzip',
'application/gzip',
'application/sql'
].includes(file.type)
) {
throw new Error('无效的数据库备份文件');
}
}
if (['public', 'private'].includes(type)) {
if (file.type != 'application/x-tar') {
throw new Error(`无效的${type === 'public' ? '公共' : '私有'}文件备份文件`);
}
}
if (type === 'config') {
if (file.type != 'application/json') {
throw new Error(`无效的站点配置文件`);
}
}
}
}
};
</script>

View File

@ -1,105 +0,0 @@
<template>
<div class="mt-2 space-y-2 divide-y">
<AppUpdateCard
v-for="(app, index) in appsWithUpdates"
:key="app.app"
@click.native.self="toggleApp(app)"
v-model:app="appsWithUpdates[index]"
:selected="selectedApps.map(a => a.app).includes(app.app)"
:uninstall="false"
:selectable="true"
/>
<AppUpdateCard
v-for="(app, index) in removedApps"
:key="app.name"
@click.native.self="toggleApp(app)"
v-model:app="removedApps[index]"
:selected="selectedApps.map(a => a.app).includes(app.app)"
:uninstall="true"
/>
</div>
</template>
<script>
import AppUpdateCard from './AppUpdateCard.vue';
export default {
name: 'BenchAppUpdates',
props: ['apps', 'removedApps'],
components: {
AppUpdateCard
},
data() {
return {
selectedApps: []
};
},
mounted() {
// Select all apps by default
this.selectedApps = this.appsWithUpdates.map(a => {
return { app: a.app, release: a.next_release };
});
this.$emit('update:selectedApps', this.selectedApps);
},
methods: {
toggleApp(app) {
if (!this.selectedApps.map(a => a.app).includes(app.app)) {
this.selectedApps.push({ app: app.app, release: app.next_release });
this.$emit('update:selectedApps', this.selectedApps);
} else {
this.selectedApps = this.selectedApps.filter(a => a.app !== app.app);
this.$emit('update:selectedApps', this.selectedApps);
}
}
},
computed: {
appsWithUpdates() {
return this.apps.filter(app => app.update_available);
}
},
watch: {
appsWithUpdates: {
handler(apps) {
apps.map(app => {
this.selectedApps
.filter(a => a.app == app.app)
.map(a => (a.release = app.next_release));
});
},
deep: true,
immediate: true
},
removedApps: {
handler(apps) {
apps.map(app => {
this.selectedApps
.filter(a => a.app == app.app)
.map(a => (a.release = app.next_release));
});
},
deep: true,
immediate: true
},
selectedApps: {
handler(apps) {
// Hardcoded for now, need a better way
// to manage such dependencies (#TODO)
// If updating JERP, must update Jingrow with it
let jingrowApp = this.apps.filter(a => a.app == 'jingrow');
let jingrowUpdateAvailable =
jingrowApp.length == 1 && jingrowApp[0].update_available;
if (
apps.map(a => a.app).includes('jerp') &&
!apps.map(a => a.app).includes('jingrow') &&
jingrowUpdateAvailable
) {
apps.push({ app: 'jingrow', release: jingrowApp[0].next_release });
}
},
deep: true,
immediate: true
}
}
};
</script>

View File

@ -1,44 +0,0 @@
<template>
<div class="mt-2 space-y-2 divide-y max-h-96 overflow-auto">
<SiteUpdateCard
v-for="site in sites"
:key="site.name"
@click.native.self="toggleSite(site)"
:site="site.name"
:selected="selectedSites.map(s => s.name).includes(site.name)"
:selectable="true"
v-model:selectedSites="selectedSites"
/>
</div>
</template>
<script>
import SiteUpdateCard from './SiteUpdateCard.vue';
export default {
name: 'BenchSiteUpdates',
props: ['sites'],
components: {
SiteUpdateCard
},
data() {
return {
selectedSites: []
};
},
mounted() {
// Select all sites by default
this.$emit('update:selectedSites', []);
},
methods: {
toggleSite(site) {
if (!this.selectedSites.includes(site)) {
this.selectedSites.push(site);
this.$emit('update:selectedSites', this.selectedSites);
} else {
this.selectedSites = this.selectedSites.filter(a => a !== site);
this.$emit('update:selectedSites', this.selectedSites);
}
}
}
};
</script>

View File

@ -1,35 +0,0 @@
<template>
<Dialog :options="{ title: 'Add card to create sites' }" v-model="showDialog">
<template v-slot:body-content>
<StripeCard
class="mb-1"
@complete="
showDialog = false;
$emit('success');
"
/>
</template>
</Dialog>
</template>
<script>
import StripeCard from '@/components/StripeCard.vue';
export default {
name: 'BillingInformationDialog',
props: ['modelValue'],
emits: ['update:modelValue', 'success'],
components: {
StripeCard
},
computed: {
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
}
}
};
</script>

View File

@ -1,201 +0,0 @@
<template>
<div>
<FormControl
v-if="step == 'Get Amount'"
class="mb-2"
label="Credits"
v-model.number="creditsToBuy"
name="amount"
autocomplete="off"
type="number"
:min="minimumAmount"
/>
<label
class="block"
:class="{
'h-0.5 opacity-0': step != 'Add Card Details',
'mt-4': step == 'Add Card Details'
}"
>
<span class="text-sm leading-4 text-gray-700">
Credit or Debit Card
</span>
<div
class="form-input mt-2 block w-full py-2 pl-3"
ref="card-element"
></div>
<ErrorMessage class="mt-1" :message="cardErrorMessage" />
</label>
<FormControl
v-if="step == 'Get Amount'"
label="Total Amount"
disabled
v-model="total"
name="total"
autocomplete="off"
type="number"
/>
<div v-if="step == 'Setting up Stripe'" class="mt-8 flex justify-center">
<Spinner class="h-4 w-4 text-gray-600" />
</div>
<ErrorMessage
class="mt-2"
:message="$resources.createPaymentIntent.error || errorMessage"
/>
<div class="mt-4 flex w-full justify-between">
<StripeLogo />
<div v-if="step == 'Get Amount'">
<Button
variant="solid"
@click="$resources.createPaymentIntent.submit()"
:loading="$resources.createPaymentIntent.loading"
>
Next
</Button>
</div>
<div v-if="step == 'Add Card Details'">
<Button @click="$emit('cancel')"> Cancel </Button>
<Button
class="ml-2"
variant="solid"
@click="onBuyClick"
:loading="paymentInProgress"
>
Buy Credits
</Button>
</div>
</div>
</div>
</template>
<script>
import StripeLogo from '@/components/StripeLogo.vue';
import { loadStripe } from '@stripe/stripe-js';
export default {
name: 'BuyPrepaidCredits',
components: {
StripeLogo
},
props: {
minimumAmount: {
default: 0
}
},
mounted() {
this.updateTotal();
},
watch: {
creditsToBuy() {
this.updateTotal();
}
},
data() {
return {
step: 'Get Amount', // Get Amount / Add Card Details
clientSecret: null,
creditsToBuy: this.minimumAmount || null,
total: this.minimumAmount,
cardErrorMessage: null,
errorMessage: null,
paymentInProgress: false
};
},
resources: {
createPaymentIntent() {
return {
url: 'jcloud.api.billing.create_payment_intent_for_buying_credits',
params: {
amount: this.creditsToBuy
},
validate() {
if (
this.creditsToBuy < this.minimumAmount &&
!this.$account.team.jerp_partner
) {
return `Amount must be greater than ${this.minimumAmount}`;
}
},
async onSuccess(data) {
this.step = 'Setting up Stripe';
let { publishable_key, client_secret } = data;
this.clientSecret = client_secret;
this.stripe = await loadStripe(publishable_key);
this.elements = this.stripe.elements();
let theme = this.$theme;
let style = {
base: {
color: theme.colors.black,
fontFamily: theme.fontFamily.sans.join(', '),
fontSmoothing: 'antialiased',
fontSize: '13px',
'::placeholder': {
color: theme.colors.gray['400']
}
},
invalid: {
color: theme.colors.red['600'],
iconColor: theme.colors.red['600']
}
};
this.card = this.elements.create('card', {
hidePostalCode: true,
style: style,
classes: {
complete: '',
focus: 'bg-gray-100'
}
});
this.step = 'Add Card Details';
this.$nextTick(() => {
this.card.mount(this.$refs['card-element']);
});
this.card.addEventListener('change', event => {
this.cardErrorMessage = event.error?.message || null;
});
this.card.addEventListener('ready', () => {
this.ready = true;
});
}
};
}
},
methods: {
updateTotal() {
if (this.$account.team.currency === 'CNY') {
this.total = Number(
(
this.creditsToBuy +
this.creditsToBuy * this.$account.billing_info.gst_percentage
).toFixed(2)
);
} else {
this.total = this.creditsToBuy;
}
},
setupStripe() {
this.$resources.createPaymentIntent.submit();
},
async onBuyClick() {
this.paymentInProgress = true;
let payload = await this.stripe.confirmCardPayment(this.clientSecret, {
payment_method: {
card: this.card
}
});
this.paymentInProgress = false;
if (payload.error) {
this.errorMessage = payload.error.message;
} else {
this.$emit('success');
this.errorMessage = null;
this.creditsToBuy = null;
}
}
}
};
</script>

View File

@ -1,15 +0,0 @@
<template>
<div
class="min-h-full w-full flex-col md:w-2/3 md:border-l"
:class="showDetails ? 'flex' : 'hidden md:flex'"
:style="{ height: 'calc(100vh - 12rem)' }"
>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'CardDetails',
props: ['showDetails']
};
</script>

View File

@ -1,24 +0,0 @@
<template>
<div class="flex flex-col rounded-lg border md:flex-row">
<Card
:title="title"
:subtitle="subtitle"
class="w-full border-none shadow-none md:w-1/3"
:class="{ 'hidden md:flex': showDetails }"
:style="{ height: viewportWidth > 768 ? 'calc(100vh - 12rem)' : null }"
>
<slot></slot>
<template #actions>
<slot name="actions"></slot>
</template>
</Card>
<slot name="details"></slot>
</div>
</template>
<script>
export default {
name: 'CardWithDetails',
props: ['title', 'subtitle', 'showDetails'],
inject: ['viewportWidth']
};
</script>

View File

@ -1,116 +0,0 @@
<template>
<Dialog
v-if="app"
v-model="show"
:options="{ title: `Change branch for ${app.title}` }"
>
<template v-slot:body-content>
<div class="flex flex-col items-center">
<Button
class="w-min"
v-if="$resources.branches.loading"
:loading="true"
loadingText="Loading..."
/>
<FormControl
v-else
class="w-full"
label="Select Branch"
type="select"
:options="branchList()"
v-model="selectedBranch"
/>
<ErrorMessage
class="mt-2 w-full"
:message="$resources.changeBranch.error"
/>
</div>
</template>
<template #actions>
<Button
v-if="!$resources.branches.loading"
class="w-full"
variant="solid"
label="Change Branch"
:loading="$resources.changeBranch.loading"
@click="changeBranch()"
/>
</template>
</Dialog>
</template>
<script>
export default {
name: 'ChangeAppBranchDialog',
emits: ['update:app'],
props: ['bench', 'app'],
data() {
return {
selectedBranch: null
};
},
resources: {
branches() {
return {
url: 'jcloud.api.bench.branch_list'
};
},
changeBranch() {
return {
url: 'jcloud.api.bench.change_branch',
onSuccess() {
window.location.reload();
},
validate() {
if (this.selectedBranch == this.app.branch) {
return 'Please select a different branch';
}
}
};
}
},
watch: {
app(value) {
if (value) {
this.selectedBranch = value.branch;
this.$resources.branches.submit({
name: this.bench,
app: value.name
});
}
}
},
methods: {
branchList() {
if (this.$resources.branches.loading || !this.$resources.branches.data) {
return [];
}
return this.$resources.branches.data.map(d => d.name);
},
changeBranch() {
this.$resources.changeBranch.submit({
name: this.bench,
app: this.app.name,
to_branch: this.selectedBranch
});
},
dialogClosed() {
this.$emit('update:app', null);
this.$resources.changeBranch.reset();
}
},
computed: {
show: {
get() {
return Boolean(this.app && this.bench);
},
set(value) {
if (!value) {
this.dialogClosed();
}
}
}
}
};
</script>

View File

@ -1,115 +0,0 @@
<template>
<div>
<ErrorMessage :message="$resources.getAppPlans.error" />
<Button
v-if="
$resources.getAppPlans.loading ||
$resources.getMarketplaceAppInfo.loading
"
:loading="true"
loadingText="Loading Plans..."
></Button>
<div v-if="plans && appInfo" class="mb-6 flex flex-row items-center">
<Avatar
class="mr-2"
size="lg"
shape="square"
:image="appInfo.image"
:label="appInfo.title"
/>
<div class="flex flex-col">
<h4 class="text-xl font-semibold text-gray-900">{{ appInfo.title }}</h4>
<p class="text-base text-gray-600">Choose your plans</p>
</div>
</div>
<div v-if="plans" class="mx-auto grid grid-cols-1 gap-2 md:grid-cols-3">
<AppPlanCard
v-for="plan in plans"
:editable="editable"
:plan="plan"
:key="plan.name"
:selected="selectedPlan == plan"
@click.native="handleCardClick(plan)"
/>
</div>
</div>
</template>
<script>
import AppPlanCard from '@/components/AppPlanCard.vue';
export default {
name: 'ChangeAppPlanSelector',
components: {
AppPlanCard
},
props: ['app', 'group', 'jingrowVersion', 'currentPlan', 'editable'],
emits: ['change'],
data() {
return {
selectedPlan: null
};
},
resources: {
getAppPlans() {
return {
url: 'jcloud.api.marketplace.get_app_plans',
params: {
app: this.app,
include_disabled: false,
release_group: this.group,
jingrow_version: this.jingrowVersion
},
onSuccess(plans) {
if (this.currentPlan) {
for (let plan of plans) {
if (plan.name === this.currentPlan) {
this.selectedPlan = plan;
break;
}
}
}
},
auto: true
};
},
getMarketplaceAppInfo() {
return {
url: 'jcloud.api.marketplace.get_app_info',
params: {
app: this.app
},
auto: true
};
}
},
methods: {
handleCardClick(plan) {
this.selectedPlan = plan;
this.$emit('change', plan);
}
},
computed: {
plans() {
if (
this.$resources.getAppPlans.data &&
!this.$resources.getAppPlans.loading
) {
return this.$resources.getAppPlans.data;
}
},
appInfo() {
if (
!this.$resources.getMarketplaceAppInfo.loading &&
this.$resources.getMarketplaceAppInfo.data
) {
return this.$resources.getMarketplaceAppInfo.data;
}
}
}
};
</script>

View File

@ -1,135 +0,0 @@
<template>
<Dialog
:options="{
title: 'Change Payment Mode',
actions: [
{
label: 'Change',
variant: 'solid',
loading: $resources.changePaymentMode.loading,
onClick: () => $resources.changePaymentMode.submit()
}
]
}"
:modelValue="modelValue"
@update:modelValue="$emit('update:modelValue', $event)"
>
<template v-slot:body-content>
<FormControl
label="Select Payment Mode"
type="select"
:options="paymentModeOptions"
v-model="paymentMode"
/>
<p class="mt-2 text-base text-gray-600 mb-5">
{{ paymentModeDescription }}
</p>
<ErrorMessage
class="mt-2"
:message="$resources.changePaymentMode.error"
/>
</template>
</Dialog>
<BillingInformationDialog
v-model="showBillingInformationDialog"
v-if="showBillingInformationDialog"
/>
<PrepaidCreditsDialog
v-if="showPrepaidCreditsDialog"
v-model:show="showPrepaidCreditsDialog"
:minimumAmount="$account.team.currency == 'CNY' ? 0.01 : 0.01"
@success="
() => {
$resources.upcomingInvoice.reload();
showPrepaidCreditsDialog = false;
}
"
/>
</template>
<script>
import { defineAsyncComponent } from 'vue';
export default {
name: 'ChangePaymentModeDialog',
props: ['modelValue'],
emits: ['update:modelValue'],
components: {
BillingInformationDialog: defineAsyncComponent(() =>
import('./BillingInformationDialog.vue')
),
PrepaidCreditsDialog: defineAsyncComponent(() =>
import('@/components/PrepaidCreditsDialog.vue')
)
},
data() {
return {
showBillingInformationDialog: false,
showPrepaidCreditsDialog: false,
paymentMode: this.$account.team.payment_mode
};
},
watch: {
show(value) {
if (!value) {
this.paymentMode = this.$account.team.payment_mode;
}
}
},
resources: {
changePaymentMode() {
return {
url: 'jcloud.api.billing.change_payment_mode',
params: {
mode: this.paymentMode
},
onSuccess() {
this.$emit('update:modelValue', false);
this.$resources.changePaymentMode.reset();
},
validate() {
if (
this.paymentMode == 'Card' &&
!this.$account.team.default_payment_method
) {
this.$emit('update:modelValue', false);
this.showBillingInformationDialog = true;
}
if (
this.paymentMode == 'Prepaid Credits' &&
this.$account.balance === 0
) {
this.$emit('update:modelValue', false);
this.showPrepaidCreditsDialog = true;
}
if (
this.paymentMode == 'Paid By Partner' &&
!this.$account.team.partner_email
) {
return 'Please add a partner first from Partner section';
}
}
};
}
},
computed: {
paymentModeDescription() {
return {
Card: `Your card will be charged for monthly subscription`,
'Prepaid Credits': `You will be charged from your account balance for monthly subscription`,
'Paid By Partner': `Your partner will be charged for monthly subscription`
}[this.paymentMode];
},
paymentModeOptions() {
if (
this.$account.team.jerp_partner ||
!this.$account.team.partner_email
) {
return ['Card', 'Prepaid Credits'];
}
return ['Card', 'Prepaid Credits', 'Paid By Partner'];
}
}
};
</script>

View File

@ -1,59 +0,0 @@
<template>
<div class="relative rounded-lg border-2 border-gray-200 bg-gray-100 p-3">
<div class="select-all break-all text-xs text-gray-800">
<pre
:class="{
'whitespace-pre-wrap': breakLines,
'overflow-x-auto': !breakLines
}"
:style="
!breakLines
? 'scrollbar-width: none; -ms-overflow-style: none; -webkit-scrollbar: none;'
: ''
"
>{{ textContent }}</pre
>
</div>
<button
class="absolute right-2 top-2 rounded-sm border border-gray-200 bg-white p-1 text-xs text-gray-600"
variant="outline"
@click="copyTextContentToClipboard"
>
{{ copied ? 'copied' : 'copy' }}
</button>
</div>
</template>
<script>
import { toast } from 'vue-sonner';
export default {
props: {
textContent: {
type: String,
required: true
},
breakLines: {
type: Boolean,
default: true
}
},
data() {
return {
copied: false
};
},
methods: {
copyTextContentToClipboard() {
const clipboard = window.navigator.clipboard;
clipboard.writeText(this.textContent).then(() => {
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 4000);
toast.success('Copied to clipboard!');
});
}
}
};
</script>

View File

@ -1,114 +0,0 @@
<template>
<Dialog
:modelValue="show"
:options="{ size: 'xl', position: 'top' }"
@after-leave="
() => {
$emit('close', true);
filteredOptions = [];
}
"
>
<template #body>
<Combobox @update:model-value="onSelection">
<ComboboxInput
@keyup.enter="show = false"
placeholder="Search for sites, bench, groups and servers"
class="w-full border-none bg-transparent px-4 text-base text-gray-800 placeholder-gray-500 focus:ring-0"
@input="onInput"
autocomplete="off"
/>
<ComboboxOptions
@mousedown="show = false"
class="max-h-96 overflow-auto border-t border-gray-100"
static
>
<ComboboxOption
v-for="option in filteredOptions"
:key="`${option.name}`"
v-slot="{ active }"
:value="option"
>
<div
class="flex w-full items-center px-4 py-2 text-base text-gray-900"
:class="{ 'bg-gray-200': active }"
>
<span> {{ option.title }}&nbsp; </span>
<span class="ml-auto text-gray-600">
{{ option.pagetype }}
</span>
</div>
</ComboboxOption>
</ComboboxOptions>
</Combobox>
</template>
</Dialog>
</template>
<script>
import {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption
} from '@headlessui/vue';
import Fuse from 'fuse.js/dist/fuse.basic.esm';
import { debounce } from 'lodash';
export default {
name: 'CommandPalette',
props: {
show: false
},
data() {
return {
filteredOptions: []
};
},
components: {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption
},
mounted() {
this.makeFuse();
},
methods: {
onInput: debounce(function (e) {
let query = e.target.value;
if (query) {
this.filteredOptions = this.fuse
.search(query)
.map(result => result.item);
}
}, 200),
onSelection(value) {
if (value) {
this.$router.push(value.route);
}
},
async makeFuse() {
let list = await this.$call('jcloud.api.account.fuse_list');
let fuse_list = list;
for (let item of fuse_list) {
item.route =
`/${
item.pagetype.toLowerCase() + (item.pagetype === 'Bench' ? 'es' : 's')
}/` +
item.route +
(item.pagetype === 'Bench' ? '/sites' : '/overview');
}
const options = {
limit: 20,
includeScore: true,
shouldSort: true,
minMatchCharLength: 3,
keys: ['title']
};
this.fuse = new Fuse(fuse_list, options);
}
}
};
</script>

View File

@ -1,341 +0,0 @@
<template>
<Card :title="title || 'Site Config'">
<template #actions>
<Button
class="mr-2"
:loading="$resources.configData.loading"
v-if="isDirty"
@click="
() => {
$resources.configData.reload().then(() => {
isDirty = false;
});
}
"
>
Discard changes
</Button>
<Button
variant="solid"
v-if="isDirty"
@click="updateConfig"
:loading="$resources.updateConfig.loading"
>
Save changes
</Button>
</template>
<div class="flex space-x-4">
<div class="w-full shrink-0 space-y-4 md:w-1/2">
<div class="ml-2">
<ErrorMessage :message="$resources.updateConfig.error" />
<div
v-if="$resources.configData?.data?.length"
v-for="config in $resources.configData.data"
:key="config.key"
class="mt-2 flex"
>
<FormControl
:label="getStandardConfigTitle(config.key)"
v-model="config.value"
@click="
config.type === 'Password' ? (config.value = '') : null;
config.type === 'Password' ? (isDirty = true) : null;
"
@input="isDirty = true"
class="flex-1"
/>
<Button
class="ml-2 mt-5"
icon="x"
variant="ghost"
@click="removeConfig(config)"
/>
</div>
<p v-else class="my-2 text-base text-gray-600">
No keys added. Click on Add Key to add one.
</p>
<Button class="mt-4" @click="showAddConfigKeyDialog = true"
>Add Key</Button
>
</div>
</div>
<div
class="hidden h-fit max-w-full flex-1 overflow-x-scroll whitespace-pre-line rounded bg-gray-100 p-4 font-mono text-base md:block"
>
<div v-if="configName" class="mb-4">{{ configName }}</div>
<div v-html="configPreview"></div>
</div>
<Dialog
:options="{
title: 'Add Config Key',
actions: [
{
label: 'Add Key',
variant: 'solid',
onClick: addConfig
}
]
}"
v-model="showAddConfigKeyDialog"
>
<template v-slot:body-content>
<div class="space-y-4">
<div>
<span class="mb-1 block text-xs text-gray-600">Key</span>
<Autocomplete
placeholder="Key"
:options="getStandardConfigKeys"
v-model="chosenStandardConfig"
@update:modelValue="handleAutocompleteSelection"
/>
</div>
<FormControl
v-if="showCustomKeyInput"
v-model="newConfig.key"
label="Custom Key"
class="w-full"
@change="isDirty = true"
/>
<FormControl
label="Type"
v-model="newConfig.type"
type="select"
:disabled="chosenStandardConfig && !showCustomKeyInput"
:options="[
'String',
'Number',
'JSON',
'Boolean',
chosenStandardConfig?.value !== 'custom_key' ? 'Password' : null
]"
@change="isDirty = true"
/>
<FormControl
v-bind="configInputProps()"
v-model="newConfig.value"
label="Value"
class="w-full"
@change="isDirty = true"
/>
</div>
</template>
</Dialog>
</div>
</Card>
</template>
<script>
import { Autocomplete } from 'jingrow-ui';
export default {
name: 'ConfigEditor',
components: {
Autocomplete
},
props: [
'title',
'subtitle',
'configName',
'configData',
'updateConfigMethod'
],
data() {
return {
isDirty: false,
showCustomKeyInput: false,
showAddConfigKeyDialog: false,
chosenStandardConfig: {
title: '',
key: ''
},
newConfig: {
key: '',
value: '',
type: 'String'
}
};
},
resources: {
configData() {
return this.configData();
},
updateConfig() {
function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
const updatedConfig = this.$resources.configData.data.map(d => {
const value = d.value;
if (!isNaN(value)) d.type = 'Number';
else if (isValidJSON(value)) d.type = 'JSON';
else if (d.type === 'Password') d.type = 'Password';
else d.type = 'String';
return {
key: d.key,
value,
type: d.type
};
});
return {
...this.updateConfigMethod(updatedConfig),
async validate() {
let keys = updatedConfig.map(d => d.key);
if (keys.length !== [...new Set(keys)].length) {
return 'Duplicate key';
}
this.$resources.validateKeys.submit({
keys: JSON.stringify(keys)
});
let invalidKeys = this.$resources.validateKeys.data;
if (invalidKeys?.length > 0) {
return `Invalid key: ${invalidKeys.join(', ')}`;
}
for (let config of updatedConfig) {
if (config.type === 'JSON') {
try {
JSON.parse(JSON.stringify(config.value));
} catch (error) {
return `Invalid JSON -- ${error}`;
}
} else if (config.type === 'Number') {
try {
Number(config.value);
} catch (error) {
return 'Invalid Number';
}
}
}
},
onSuccess() {
this.isDirty = false;
this.$resources.configData.reload();
}
};
},
standardConfigKeys: {
url: 'jcloud.api.config.standard_keys',
cache: 'standardConfigKeys',
auto: true
},
validateKeys: {
url: 'jcloud.api.config.is_valid'
}
},
computed: {
configPreview() {
let obj = {};
for (let d of this.$resources.configData.data) {
let value = d.value;
if (['Boolean', 'Number'].includes(d.type)) {
value = Number(d.value);
} else if (d.type === 'JSON') {
try {
value = JSON.parse(d.value);
} catch (error) {
value = {};
}
}
obj[d.key] = value;
}
return JSON.stringify(obj, null, '&nbsp; ');
},
getStandardConfigKeys() {
return [
{
group: 'Custom',
items: [{ label: 'Create a custom key', value: 'custom_key' }]
},
{
group: 'Standard',
items: this.$resources.standardConfigKeys.data.map(d => ({
label: d.title,
value: d.key
}))
}
];
}
},
methods: {
configInputProps() {
let type = {
String: 'text',
Password: 'text',
Number: 'number',
JSON: 'textarea',
Boolean: 'select'
}[this.newConfig.type];
return {
type,
options: this.newConfig.type === 'Boolean' ? ['1', '0'] : null
};
},
addConfig() {
this.$resources.configData.data.push({
key: this.getStandardConfigKey(this.newConfig.key),
value: this.newConfig.value,
type: this.newConfig.type
});
this.isDirty = true;
this.showAddConfigKeyDialog = false;
},
handleAutocompleteSelection() {
if (this.chosenStandardConfig?.value === 'custom_key') {
this.showCustomKeyInput = true;
} else {
this.showCustomKeyInput = false;
this.newConfig.type = this.getStandardConfigType(
this.chosenStandardConfig?.value
);
}
if (this.newConfig.type === 'Boolean') {
this.newConfig.value = 0;
} else if (this.newConfig.type === 'JSON') {
this.newConfig.value = '{}';
} else {
this.newConfig.value = '';
}
this.newConfig.key = this.chosenStandardConfig?.value || '';
},
getStandardConfigType(key) {
return (
this.$resources.standardConfigKeys.data.find(d => d.key === key)
?.type || 'String'
);
},
getStandardConfigKey(key) {
return (
this.$resources.standardConfigKeys.data.find(d => d.title === key)
?.key || key
);
},
getStandardConfigTitle(key) {
return (
this.$resources.standardConfigKeys.data.find(d => d.key === key)
?.title || key
);
},
removeConfig(config) {
const index = this.$resources.configData.data.indexOf(config);
if (index > -1) this.$resources.configData.data.splice(index, 1);
this.isDirty = true;
},
updateConfig() {
if (this.isDirty) {
this.$resources.updateConfig.submit();
} else {
this.isDirty = false;
}
}
}
};
</script>

View File

@ -1,63 +0,0 @@
<script setup>
import { getCurrentInstance, ref } from 'vue';
const app = getCurrentInstance();
const confirmDialogs = ref([]);
function confirm(dialog) {
dialog.id = confirmDialogs.value.length;
dialog.show = true;
confirmDialogs.value.push(dialog);
}
function removeConfirmDialog(dialog) {
confirmDialogs.value = confirmDialogs.value.filter(
_dialog => dialog !== _dialog
);
}
function onDialogAction(dialog) {
let closeDialog = () => removeConfirmDialog(dialog);
dialog.action(closeDialog);
}
app.appContext.config.globalProperties.$confirm = confirm;
</script>
<template>
<div>
<Dialog
v-for="dialog in confirmDialogs"
v-model="dialog.show"
@close="removeConfirmDialog(dialog)"
:key="dialog.id"
:options="{
title: dialog.title,
actions: [
{
label: dialog.actionLabel || 'Submit',
theme: dialog.actionColor,
variant: dialog.actionVariant || 'solid',
onClick: () => onDialogAction(dialog),
loading: dialog.resource?.loading
},
{
label: 'Cancel',
onClick: () => removeConfirmDialog(dialog)
}
]
}"
>
<template v-slot:body-content>
<div class="prose">
<p class="text-base" v-html="dialog.message"></p>
</div>
<ErrorMessage
class="mt-2"
v-if="dialog.resource"
:message="dialog.resource.error"
/>
</template>
</Dialog>
</div>
</template>

View File

@ -1,17 +0,0 @@
<template>
<div class="space-y-4">
<div v-for="item in items" :key="item.label">
<div class="text-sm text-gray-700">{{ item.label }}</div>
<div class="mt-1 rounded bg-gray-100 px-3 py-1 text-base text-gray-900">
{{ item.value }}
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DescriptionList',
props: ['items']
};
</script>

View File

@ -1,19 +0,0 @@
<template>
<ul class="space-y-2 text-sm text-gray-700">
<li
class="flex flex-row justify-items-center"
v-for="feature in features"
:key="feature"
>
<CircularCheckIcon class="mr-2" />
{{ feature }}
</li>
</ul>
</template>
<script>
export default {
name: 'FeatureList',
props: ['features']
};
</script>

View File

@ -1,118 +0,0 @@
<template>
<div>
<input
ref="input"
type="file"
:accept="fileTypes"
class="hidden"
@change="onFileAdd"
/>
<slot
v-bind="{
file,
uploading,
progress,
uploaded,
message,
error,
total,
success,
openFileSelector
}"
/>
</div>
</template>
<script>
import FileUploader from '@/controllers/fileUploader';
import S3FileUploader from '@/controllers/s3FileUploader';
import { trypromise } from '@/utils';
export default {
name: 'FileUploader',
props: ['fileTypes', 'uploadArgs', 's3', 'type', 'fileValidator'],
data() {
return {
uploader: null,
uploading: false,
uploaded: 0,
error: null,
message: '',
total: 0,
file: null,
finishedUploading: false
};
},
computed: {
progress() {
let value = Math.floor((this.uploaded / this.total) * 100);
return isNaN(value) ? 0 : value;
},
success() {
return this.finishedUploading && !this.error;
}
},
methods: {
openFileSelector() {
this.$refs['input'].click();
},
async onFileAdd(e) {
this.error = null;
this.file = e.target.files[0];
if (this.file && this.fileValidator) {
let [error, _] = await trypromise(this.fileValidator(this.file));
if (error) {
this.error = error;
}
}
if (!this.error) {
this.uploadFile(this.file);
}
},
async uploadFile(file) {
this.error = null;
this.uploaded = 0;
this.total = 0;
this.uploader = this.s3 ? new S3FileUploader() : new FileUploader();
this.uploader.on('start', () => {
this.uploading = true;
});
this.uploader.on('progress', data => {
this.uploaded = data.uploaded;
this.total = data.total;
});
this.uploader.on('error', () => {
this.uploading = false;
this.error = 'Error Uploading File';
});
this.uploader.on('finish', () => {
this.uploading = false;
this.finishedUploading = true;
});
this.uploader
.upload(file, this.uploadArgs || {})
.then(data => {
this.$emit('success', data);
})
.catch(error => {
this.uploading = false;
let errorMessage = 'Error Uploading File';
if (error._server_messages) {
errorMessage = JSON.parse(
JSON.parse(error._server_messages)[0]
).message;
} else if (error.exc) {
errorMessage = JSON.parse(error.exc)[0]
.split('\n')
.slice(-2, -1)[0];
}
this.error = errorMessage;
this.$emit('failure', errorMessage);
});
}
}
};
</script>

View File

@ -1,130 +0,0 @@
<template>
<div class="space-y-4">
<div
v-for="field in fields"
:key="field.fieldname"
v-show="field.condition ? field.condition() : true"
>
<div class="flex space-x-4" v-if="Array.isArray(field)">
<FormControl
v-bind="getBindProps(subfield)"
:key="subfield.fieldname"
class="w-full"
v-for="subfield in field"
/>
</div>
<FormControl v-else v-bind="getBindProps(field)" />
<ErrorMessage
class="mt-1"
v-if="requiredFieldNotSet.includes(field)"
error="This field is required"
/>
</div>
</div>
</template>
<script>
// https://github.com/eggert/tz/blob/main/backward add more if required.
const TZ_BACKWARD_COMPATBILITY_MAP = {
'Asia/Calcutta': 'Asia/Kolkata'
};
export default {
name: 'Form',
props: ['fields', 'modelValue'],
emits: ['update:modelValue'],
data() {
return {
requiredFieldNotSet: [],
guessedTimezone: ''
};
},
mounted() {
this.guessedTimezone = this.guessTimezone();
},
watch: {
fields: {
handler(new_fields) {
let timezoneFields = new_fields.filter(
f => f.fieldtype === 'Select' && f.fieldname.endsWith('_tz')
);
for (let field of timezoneFields) {
if (!field.options) {
field.options = [];
}
if (
this.guessedTimezone &&
field.options.includes(this.guessedTimezone)
) {
this.onChange(this.guessedTimezone, field);
}
}
},
deep: true
}
},
methods: {
onChange(value, field) {
this.checkRequired(field, value);
this.updateValue(field.fieldname, value);
},
updateValue(fieldname, value) {
let values = Object.assign({}, this.modelValue, {
[fieldname]: value
});
this.$emit('update:modelValue', values);
},
checkRequired(field, value) {
if (field.required) {
if (!value) {
this.requiredFieldNotSet.push(field);
return false;
} else {
this.requiredFieldNotSet = this.requiredFieldNotSet.filter(
f => f !== field
);
}
}
return true;
},
getBindProps(field) {
return {
label: field.label || field.fieldname,
type: this.getInputType(field),
options: field.options,
name: field.fieldname,
modelValue: this.modelValue[field.fieldname],
disabled: field.disabled,
required: field.required || false,
rows: field.rows,
placeholder: field.placeholder,
'onUpdate:modelValue': value => this.onChange(value, field),
onBlur: e => this.checkRequired(field, e)
};
},
getInputType(field) {
return {
Data: 'text',
Int: 'number',
Select: 'select',
Check: 'checkbox',
Password: 'password',
Text: 'textarea',
Date: 'date'
}[field.fieldtype || 'Data'];
},
guessTimezone() {
try {
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (TZ_BACKWARD_COMPATBILITY_MAP[tz]) {
return TZ_BACKWARD_COMPATBILITY_MAP[tz];
}
return tz;
} catch (e) {
console.error("Couldn't guess timezone", e);
return null;
}
}
}
};
</script>

View File

@ -1,95 +0,0 @@
<template>
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter0_ddd_28_966)">
<rect x="4" y="3" width="40" height="40" rx="10" fill="#590EFA" />
</g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M25.415 13.5C27.411 13.5 29.2176 14.3109 30.5234 15.6212C29.7284 15.7907 28.9722 16.0399 28.2642 16.4062C26.1108 17.5202 24.5787 19.6053 23.494 23.173C23.44 23.3504 23.3876 23.5261 23.3365 23.7L12.0706 23.7C12.6494 20.3849 15.4531 17.8345 18.8863 17.6441C20.039 15.1952 22.529 13.5 25.415 13.5ZM12.0005 25.7375C12.3752 29.3476 15.3672 32.1803 19.046 32.3051V32.3104H19.0698L19.0998 32.2983C19.9271 31.9628 20.5108 31.43 20.9884 30.6753C21.4839 29.8923 21.8606 28.8779 22.2438 27.5888C22.4117 27.024 22.5758 26.4246 22.752 25.7811L22.7521 25.7808L22.7521 25.7807L22.764 25.7375L12.0005 25.7375ZM29.2004 18.2158C29.9705 17.8175 30.8419 17.5899 31.8545 17.4615C32.2821 18.3068 32.548 19.2477 32.6118 20.2437C34.6288 21.3236 36.0007 23.451 36.0007 25.8988C36.0007 29.383 33.2211 32.218 29.7583 32.3071V32.3104H22.329C22.4637 32.1358 22.5905 31.9538 22.7101 31.7648C23.3634 30.7323 23.8038 29.4915 24.1968 28.1692C24.3715 27.5817 24.5419 26.9594 24.7172 26.3189L24.7173 26.3187L24.7173 26.3186L24.7877 26.0615C24.9895 25.3258 25.2014 24.5613 25.4433 23.7656C26.4384 20.4927 27.7142 18.9847 29.2004 18.2158Z"
fill="white"
/>
<defs>
<filter
id="filter0_ddd_28_966"
x="0.233733"
y="0.04079"
width="47.5325"
height="47.5325"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feMorphology
radius="0.269019"
operator="dilate"
in="SourceAlpha"
result="effect1_dropShadow_28_966"
/>
<feOffset dy="0.807057" />
<feGaussianBlur stdDeviation="1.74862" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_28_966"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="0.13451" />
<feGaussianBlur stdDeviation="0.13451" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.07 0"
/>
<feBlend
mode="normal"
in2="effect1_dropShadow_28_966"
result="effect2_dropShadow_28_966"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="0.0672548" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.19 0"
/>
<feBlend
mode="normal"
in2="effect2_dropShadow_28_966"
result="effect3_dropShadow_28_966"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect3_dropShadow_28_966"
result="shape"
/>
</filter>
</defs>
</svg>
</template>

View File

@ -1,32 +0,0 @@
<template>
<div v-if="$account.user">
<Button
v-if="$account.user.user_type == 'System User'"
@click="dialogOpen = true"
>
Impersonate Team
</Button>
<Dialog v-model="dialogOpen" :options="{ title: 'Impersonate Team' }">
<template v-slot:body-content>
<Input label="Team" v-model="team" />
</template>
<template v-slot:actions>
<Button appearance="primary" @click="$account.switchToTeam(team)">
Impersonate
</Button>
</template>
</Dialog>
</div>
</template>
<script>
export default {
name: 'ImpersonateTeam',
data() {
return {
dialogOpen: false,
team: null
};
}
};
</script>

View File

@ -1,39 +0,0 @@
<template>
<Card v-if="invoice" :title="title">
<template #actions-left>
<Button route="/billing/invoices"> Back </Button>
</template>
<InvoiceUsageTable :invoice="invoice" @pg="pg = $event" />
</Card>
</template>
<script>
import InvoiceUsageTable from './InvoiceUsageTable.vue';
export default {
name: 'InvoiceUsageCard',
props: ['invoice'],
components: {
InvoiceUsageTable
},
data() {
return {
pg: null
};
},
computed: {
title() {
let pg = this.pg;
if (!pg) {
return '';
}
if (!pg.period_start || !pg.period_end) {
return `Invoice Details for ${this.invoice}`;
}
let periodStart = this.$date(pg.period_start);
let periodEnd = this.$date(pg.period_end);
let start = periodStart.toLocaleString({ month: 'long', day: 'numeric' });
let end = periodEnd.toLocaleString({ month: 'short', day: 'numeric' });
return `Invoice for ${start} - ${end} ${periodEnd.year}`;
}
}
};
</script>

View File

@ -1,150 +0,0 @@
<template>
<div>
<div v-if="pg" class="overflow-x-auto">
<table class="text w-full text-sm">
<thead>
<tr class="text-gray-600">
<th class="border-b py-3 pr-2 text-left font-normal">
Description
</th>
<th class="border-b py-3 pr-2 text-left font-normal">Site</th>
<th
class="whitespace-nowrap border-b py-3 pr-2 text-right font-normal"
>
Rate
</th>
<th class="border-b py-3 pr-2 text-right font-normal">Amount</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in pg.items" :key="row.idx">
<td class="border-b py-3 pr-2">
{{ row.description || row.document_name }}
</td>
<td class="border-b py-3 pr-2">
{{ row.site || '-' }}
</td>
<td class="border-b py-3 pr-2 text-right">
{{ row.rate }} x {{ row.quantity }}
</td>
<td class="border-b py-3 pr-2 text-right font-semibold">
{{ pg.formatted.items[i].amount }}
</td>
</tr>
</tbody>
<tfoot>
<tr v-if="pg.total_discount_amount > 0">
<td></td>
<td class="pb-2 pr-2 pt-4 text-right font-semibold">
Total Without Discount
</td>
<td
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
>
{{ pg.formatted.total_before_discount }}
</td>
</tr>
<tr v-if="pg.total_discount_amount > 0">
<td></td>
<td class="pb-2 pr-2 pt-4 text-right font-semibold">
Total Discount Amount
</td>
<td
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
>
{{
pg.partner_email && pg.partner_email != $account.team.user
? 0
: pg.formatted.total_discount_amount
}}
</td>
</tr>
<tr v-if="pg.gst > 0">
<td></td>
<td class="pb-2 pr-2 pt-4 text-right font-semibold">
Total (Without Tax)
</td>
<td
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
>
{{ pg.formatted.total_before_tax }}
</td>
</tr>
<tr v-if="pg.gst > 0">
<td></td>
<td class="pb-2 pr-2 pt-4 text-right font-semibold">
IGST @ {{ Number($account.billing_info.gst_percentage * 100) }}%
</td>
<td
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
>
{{ pg.formatted.gst }}
</td>
</tr>
<tr>
<td></td>
<td class="pb-2 pr-2 pt-4 text-right font-semibold">Grand Total</td>
<td
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
>
{{
pg.partner_email && pg.partner_email != $account.team.user
? pg.formatted.total_before_discount
: pg.formatted.total
}}
</td>
</tr>
<template v-if="pg.total !== pg.amount_due && pg.pagestatus == 1">
<tr>
<td></td>
<td class="pr-2 text-right">Applied Balance:</td>
<td class="whitespace-nowrap py-3 pr-2 text-right font-semibold">
- {{ pg.formatted.applied_credits }}
</td>
</tr>
<tr>
<td></td>
<td class="pr-2 text-right">Amount Due:</td>
<td class="whitespace-nowrap py-3 pr-2 text-right font-semibold">
{{ pg.formatted.amount_due }}
</td>
</tr>
</template>
</tfoot>
</table>
</div>
<div class="py-20 text-center" v-if="$resources.pg.loading">
<Button :loading="true">Loading</Button>
</div>
</div>
</template>
<script>
export default {
name: 'InvoiceUsageTable',
props: ['invoice', 'invoiceDoc'],
resources: {
pg() {
return {
url: 'jcloud.api.billing.get_invoice_usage',
params: { invoice: this.invoice },
auto: this.invoice,
onSuccess(pg) {
this.$emit('pg', pg);
}
};
}
},
watch: {
invoice(value) {
if (value) {
this.$resources.pg.fetch();
}
}
},
computed: {
pg() {
return this.invoiceDoc || this.$resources.pg.data;
}
}
};
</script>

View File

@ -1,15 +0,0 @@
<template>
<component
:is="to ? 'router-link' : 'a'"
v-bind="to ? { to } : { href }"
class="border-b border-gray-800 text-gray-900 outline-gray-400 hover:border-gray-600 hover:text-gray-700"
>
<slot
/></component>
</template>
<script>
export default {
name: 'Link',
props: ['href', 'to']
};
</script>

View File

@ -1,30 +0,0 @@
<template>
<button
class="flex self-stretch rounded-lg border bg-white p-3 shadow hover:border-gray-300 focus:outline-none"
>
<Avatar
class="shrink-0"
size="2xl"
shape="square"
:image="app.image"
:label="app.title"
/>
<div class="ml-3 w-full">
<div class="flex items-center justify-between">
<h2 class="text-left text-xl font-bold">
{{ app.title }}
</h2>
<Badge :label="app.status" />
</div>
<p class="pt-1 text-left text-base text-gray-600">
{{ app.description }}
</p>
</div>
</button>
</template>
<script>
export default {
props: ['app']
};
</script>

View File

@ -1,184 +0,0 @@
<template>
<Card
class="md:col-span-2"
title="App Descriptions"
subtitle="Details about your app"
>
<div class="divide-y" v-if="app">
<ListItem title="Summary" :description="$sanitize(app.description)">
<template #actions>
<Button icon-left="edit" @click="showEditSummaryDialog = true">
Edit
</Button>
</template>
</ListItem>
<Dialog
:options="{
title: 'Update App Summary',
actions: [
{
label: 'Save Changes',
variant: 'solid',
loading: $resources.updateAppSummary.loading,
onClick: () => $resources.updateAppSummary.submit()
}
]
}"
v-model="showEditSummaryDialog"
>
<template v-slot:body-content>
<FormControl
label="Summary of the app"
type="textarea"
v-model="app.description"
/>
<ErrorMessage
class="mt-4"
:message="$resources.updateAppSummary.error"
/>
</template>
</Dialog>
<div class="py-3">
<ListItem title="Long Description">
<template #actions>
<Button icon-left="edit" @click="showEditDescriptionDialog = true">
Edit
</Button>
</template>
</ListItem>
<div
class="prose mt-1 text-gray-600"
v-if="app.description"
v-html="descriptionHTML"
></div>
<Dialog
:options="{
title: 'Update App Description',
size: '5xl',
actions: [
{
label: 'Save Changes',
variant: 'solid',
loading: $resources.updateAppDescription.loading,
onClick: () => $resources.updateAppDescription.submit()
}
]
}"
:dismissable="true"
v-model="showEditDescriptionDialog"
width="full"
>
<template v-slot:body-content>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<FormControl
:rows="30"
type="textarea"
v-model="app.long_description"
/>
<div class="prose" v-html="descriptionHTML"></div>
</div>
<ErrorMessage
class="mt-4"
:message="$resources.updateAppDescription.error"
/>
</template>
</Dialog>
</div>
</div>
<template #actions>
<Button
:loading="$resources.fetchReadme.loading"
@click="$resources.fetchReadme.submit()"
>
Fetch Readme
</Button>
</template>
</Card>
</template>
<script>
import MarkdownIt from 'markdown-it';
import { notify } from '@/utils/toast';
export default {
name: 'MarketplaceAppDescriptions',
props: {
app: Object
},
data() {
return {
showEditSummaryDialog: false,
showEditDescriptionDialog: false
};
},
resources: {
updateAppSummary() {
let { name, description } = this.app;
return {
url: 'jcloud.api.marketplace.update_app_summary',
params: {
name,
summary: description
},
onSuccess() {
this.notifySuccess('App Summary Updated!');
this.showEditSummaryDialog = false;
}
};
},
updateAppDescription() {
let { name, long_description } = this.app;
return {
url: 'jcloud.api.marketplace.update_app_description',
params: {
name,
description: long_description
},
onSuccess() {
this.notifySuccess('App Description Updated!');
this.showEditDescriptionDialog = false;
}
};
},
fetchReadme() {
return {
url: 'jcloud.api.marketplace.fetch_readme',
params: { name: this.app.name },
onSuccess() {
notify({
title: 'Successfully fetched latest readme',
message: 'Long description updated!',
icon: 'check',
color: 'green'
});
},
onError(e) {
notify({
title: e,
color: 'red',
icon: 'x'
});
}
};
}
},
computed: {
descriptionHTML() {
if (this.app && this.app.long_description) {
return MarkdownIt().render(this.app.long_description);
}
return '';
}
},
methods: {
notifySuccess(message) {
notify({
title: message,
icon: 'check',
color: 'green'
});
}
}
};
</script>

View File

@ -1,99 +0,0 @@
<template>
<Card title="Links" subtitle="Will be shown in marketplace">
<template #actions>
<Button icon-left="edit" @click="showEditLinksDialog = true">Edit</Button>
</template>
<Dialog
:options="{
title: 'Update Links',
actions: [
{
variant: 'solid',
label: 'Save Changes',
loading: $resources.updateAppLinks.loading,
onClick: () => $resources.updateAppLinks.submit()
}
]
}"
v-model="showEditLinksDialog"
>
<template v-slot:body-content>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<FormControl label="Website" v-model="app.website" />
<FormControl label="Support" v-model="app.support" />
<FormControl label="Documentation" v-model="app.documentation" />
<FormControl label="Privacy Policy" v-model="app.privacy_policy" />
<FormControl
label="Terms of Service"
v-model="app.terms_of_service"
/>
</div>
<ErrorMessage class="mt-4" :message="$resources.updateAppLinks.error" />
</template>
</Dialog>
<div class="divide-y" v-if="app">
<ListItem
title="Website"
:description="$sanitize(app.website || 'N/A')"
/>
<ListItem
title="Support"
:description="$sanitize(app.support || 'N/A')"
/>
<ListItem
title="Documentation"
:description="$sanitize(app.documentation || 'N/A')"
/>
<ListItem
title="Privacy Policy"
:description="$sanitize(app.privacy_policy || 'N/A')"
/>
<ListItem
title="Terms of Service"
:description="$sanitize(app.terms_of_service || 'N/A')"
/>
</div>
</Card>
</template>
<script>
import { notify } from '@/utils/toast';
export default {
name: 'MarketplaceAppLinks',
props: {
app: Object
},
data() {
return {
showEditLinksDialog: false
};
},
resources: {
updateAppLinks() {
return {
url: 'jcloud.api.marketplace.update_app_links',
params: {
name: this.app.name,
links: {
website: this.app.website,
support: this.app.support,
documentation: this.app.documentation,
privacy_policy: this.app.privacy_policy,
terms_of_service: this.app.terms_of_service
}
},
onSuccess() {
this.showEditLinksDialog = false;
notify({
title: 'Links Updated!',
icon: 'check',
color: 'green'
});
}
};
}
}
};
</script>

Some files were not shown because too many files have changed in this diff Show More