diff --git a/.github/workflows/studio-publish.yml b/.github/workflows/studio-publish.yml index 6ceeeaeff..1db81dfce 100644 --- a/.github/workflows/studio-publish.yml +++ b/.github/workflows/studio-publish.yml @@ -105,8 +105,12 @@ jobs: # Why does it fill up zfs? I don't understand what it does at all - name: Free Up ZFS Space run: | - sudo zfs list -H -o name -t snapshot | grep "snapcraft" | xargs -I{} sudo zfs destroy {} - sudo zfs list -H -o name | grep "snapcraft" | xargs -I{} sudo zfs destroy {} + # Force unmount any snapcraft-related ZFS datasets that are busy + sudo zfs list -H -o name | grep "snapcraft" | sort -r | xargs -I{} sudo zfs unmount -f {} 2>/dev/null || true + # Destroy datasets (clones) first, deepest children first + sudo zfs list -H -o name | grep "snapcraft" | sort -r | xargs -I{} sudo zfs destroy -rR {} 2>/dev/null || true + # Then destroy any remaining snapshots + sudo zfs list -H -o name -t snapshot | grep "snapcraft" | xargs -I{} sudo zfs destroy -rR {} 2>/dev/null || true if: matrix.os.clean_zfs - name: Install flatpak tools diff --git a/.github/workflows/ui-kit-publish.yml b/.github/workflows/ui-kit-publish.yml new file mode 100644 index 000000000..454d99f3d --- /dev/null +++ b/.github/workflows/ui-kit-publish.yml @@ -0,0 +1,34 @@ +name: UI Kit - Build & Publish + +on: + push: + tags: + - "ui-kit:v*" + +jobs: + publish: + runs-on: ubuntu-22.04 + steps: + - name: Check out Git repository + uses: actions/checkout@v1 + + - name: Install Node.js, NPM and Yarn + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: yarn + registry-url: 'https://registry.npmjs.org' + + - name: yarn install + run: yarn install --frozen-lockfile --network-timeout 100000 + + - name: Run tests + run: yarn workspace @beekeeperstudio/ui-kit run test + + - name: Build + run: yarn workspace @beekeeperstudio/ui-kit run build + + - name: Publish to npm + run: yarn workspace @beekeeperstudio/ui-kit npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/apps/studio/package.json b/apps/studio/package.json index 99dd37560..cf6c520ca 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "beekeeper-studio", - "version": "5.6.0-beta.3", + "version": "5.6.0-beta.8", "private": true, "description": "An easy-to use SQL query editor and database UI for Mac, Windows, and Linux", "author": { diff --git a/apps/studio/src/App.vue b/apps/studio/src/App.vue index fe22fa11b..371078328 100644 --- a/apps/studio/src/App.vue +++ b/apps/studio/src/App.vue @@ -35,7 +35,7 @@ - + diff --git a/apps/studio/src/components/connection/CommonEntraId.vue b/apps/studio/src/components/connection/CommonEntraId.vue index c90bd682f..1170c923d 100644 --- a/apps/studio/src/components/connection/CommonEntraId.vue +++ b/apps/studio/src/components/connection/CommonEntraId.vue @@ -1,5 +1,10 @@ @@ -33,6 +33,9 @@ export default { props: ['config'], mounted() { this.azureAuthEnabled = this.config?.azureAuthOptions?.azureAuthEnabled || false + if (this.authType !== 'default') { + this.showPasswordForm = false; + } }, data() { return { @@ -40,7 +43,6 @@ export default { iamAuthenticationEnabled: !!this.config.iamAuthOptions?.iamAuthenticationEnabled, authType: this.config.iamAuthOptions?.authType || this.config.azureAuthOptions?.azureAuthType || 'default', authTypes: [{ name: 'Username / Password', value: 'default' }, ...IamAuthTypes, ...AzureAuthTypes.filter(auth => auth.value === AzureAuthType.CLI)], - accountName: null, signingOut: false, errorSigningOut: null, showPasswordForm: true @@ -67,20 +69,16 @@ export default { this.showPasswordForm = false; if (typeof this.authType === 'string' && this.authType.includes('iam')) { this.iamAuthenticationEnabled = true; + this.azureAuthEnabled = false; this.config.iamAuthOptions.authType = this.authType; } else if (this.authType === AzureAuthType.CLI) { this.azureAuthEnabled = true; + this.iamAuthenticationEnabled = false; this.config.azureAuthOptions.azureAuthType = this.authType; } } } - const authId = this.config.azureAuthOptions?.authId || this.config?.authId - if (this.authType === AzureAuthType.CLI && !_.isNil(authId)) { - this.accountName = await this.connection.azureGetAccountName(authId); - } else { - this.accountName = null - } }, config() { if (this.config.azureAuthOptions.azureAuthEnabled) { diff --git a/apps/studio/src/components/connection/PostgresForm.vue b/apps/studio/src/components/connection/PostgresForm.vue index 678aba3e1..e46678bd5 100644 --- a/apps/studio/src/components/connection/PostgresForm.vue +++ b/apps/studio/src/components/connection/PostgresForm.vue @@ -21,8 +21,8 @@ - + @@ -52,7 +52,6 @@ export default { iamAuthenticationEnabled: !!this.config.iamAuthOptions?.iamAuthenticationEnabled, authType: this.config.iamAuthOptions?.authType || this.config.azureAuthOptions?.azureAuthType || 'default', authTypes: [{ name: 'Username / Password', value: 'default' }, ...IamAuthTypes, ...AzureAuthTypes.filter(auth => auth.value === AzureAuthType.CLI)], - accountName: null, signingOut: false, errorSigningOut: null, showPasswordForm: true @@ -88,13 +87,6 @@ export default { } } } - - const authId = this.config.azureAuthOptions?.authId || this.config?.authId - if (this.authType === AzureAuthType.AccessToken && !_.isNil(authId)) { - this.accountName = await this.connection.azureGetAccountName(authId); - } else { - this.accountName = null - } }, config() { if (this.config.azureAuthOptions.azureAuthEnabled) { diff --git a/apps/studio/src/components/connection/SqlServerForm.vue b/apps/studio/src/components/connection/SqlServerForm.vue index 62cd770a6..c73842de3 100644 --- a/apps/studio/src/components/connection/SqlServerForm.vue +++ b/apps/studio/src/components/connection/SqlServerForm.vue @@ -50,8 +50,8 @@ - + @@ -76,7 +76,6 @@ azureAuthEnabled: false, authType: 'default', authTypes: AzureAuthTypes, - accountName: null } }, watch: { diff --git a/apps/studio/src/components/plugins/IsolatedPluginView.vue b/apps/studio/src/components/plugins/IsolatedPluginView.vue index dbf44ad54..3fc6f48eb 100644 --- a/apps/studio/src/components/plugins/IsolatedPluginView.vue +++ b/apps/studio/src/components/plugins/IsolatedPluginView.vue @@ -68,6 +68,7 @@ export default Vue.extend({ await this.$nextTick(); if (this.shouldMountIframe) { this.mountIframe().catch((e) => { + log.error(e); this.error = e instanceof Error ? e.message : String(e); }); } else { diff --git a/apps/studio/src/components/plugins/PluginController.vue b/apps/studio/src/components/plugins/PluginController.vue index f76089a8d..a1dde3021 100644 --- a/apps/studio/src/components/plugins/PluginController.vue +++ b/apps/studio/src/components/plugins/PluginController.vue @@ -5,6 +5,9 @@ import { AppEvent } from "@/common/AppEvent"; import { NativePluginMenuItem } from "@/services/plugin"; export default Vue.extend({ + props: { + editorFontSize: Number, + }, computed: { rootBindings() { return [ @@ -19,6 +22,14 @@ export default Vue.extend({ ]; }, }, + watch: { + editorFontSize() { + this.$plugin.notifyAll({ + name: "editorFontSizeChanged", + args: { value: this.editorFontSize }, + }); + }, + }, methods: { handleChangedTheme(themeValue: string) { const data: PluginNotificationData = { diff --git a/apps/studio/src/components/sidebar/core/table_list/VirtualTableList.vue b/apps/studio/src/components/sidebar/core/table_list/VirtualTableList.vue index d4414a131..f8067d34f 100644 --- a/apps/studio/src/components/sidebar/core/table_list/VirtualTableList.vue +++ b/apps/studio/src/components/sidebar/core/table_list/VirtualTableList.vue @@ -111,11 +111,15 @@ export default Vue.extend({ if (item.expanded) { expandedMap.set(item.key, true); } + if (item.pinned) { + pinnedMap.set(item.key, true); + } } } for (const pin of this.pins) { - pinnedMap.set(pin.entity, pin); + const key = entityId(pin.schemaName, pin.entity); + pinnedMap.set(key, true); } this.schemaTables.forEach((schema: any) => { @@ -153,7 +157,7 @@ export default Vue.extend({ contextMenu: this.tableMenuOptions, parent, level: noFolder ? 0 : 1, - pinned: pinnedMap.get(table) || false, + pinned: pinnedMap.has(key) || false, loadingColumns: false, }); }); @@ -169,7 +173,7 @@ export default Vue.extend({ contextMenu: this.routineMenuOptions, parent, level: noFolder ? 0 : 1, - pinned: pinnedMap.get(routine) || false, + pinned: pinnedMap.has(key) || false, }); }); }); @@ -272,13 +276,12 @@ export default Vue.extend({ }, handleTogglePinned(entity: Entity, pinned?: boolean) { const item = this.items.find((item: Item) => item.entity === entity); + if (!item) return; + if (typeof pinned === "undefined") { pinned = !item.pinned; } - item.pinned = !item.pinned; - - if (pinned) this.$store.dispatch('pins/add', entity) - else this.$store.dispatch('pins/remove', entity) + item.pinned = pinned; }, handleScrollEnd() { this.updateTableColumnsInRange(true); diff --git a/apps/studio/src/lib/cloud/CloudClient.ts b/apps/studio/src/lib/cloud/CloudClient.ts index bc7574e8b..18c6a5666 100644 --- a/apps/studio/src/lib/cloud/CloudClient.ts +++ b/apps/studio/src/lib/cloud/CloudClient.ts @@ -35,6 +35,7 @@ const camelCaseData: AxiosResponseTransformer = (data) => { export interface CloudClientOptions { token: string, app: string, + clientVersion: string, email: string baseUrl: string, workspace?: number @@ -85,7 +86,8 @@ export class CloudClient { headers: { email: options.email, token: options.token, - app: options.app + app: options.app, + clientVersion: options.clientVersion, }, validateStatus: (status) => status < 500 }) diff --git a/apps/studio/src/lib/db/authentication/azure.ts b/apps/studio/src/lib/db/authentication/azure.ts index c63e11392..054485a3b 100644 --- a/apps/studio/src/lib/db/authentication/azure.ts +++ b/apps/studio/src/lib/db/authentication/azure.ts @@ -6,7 +6,7 @@ import {TokenCache} from '@/common/appdb/models/token_cache'; import globals from '@/common/globals'; import {AzureAuthOptions, AzureAuthType} from '../types'; import {spawn} from 'child_process' -import {getEntraOptions} from "@/lib/db/clients/utils"; +import {getEntraOptions, sanitizeCommandPath} from "@/lib/db/clients/utils"; import {IDbConnectionServer} from "@/lib/db/backendTypes"; import BksConfig from '@/common/bksConfig'; @@ -178,14 +178,14 @@ export class AzureAuthService { } return new Promise((resolve, reject) => { - const proc = spawn(options.cliPath, [ + const proc = spawn(sanitizeCommandPath(options.cliPath), [ 'account', 'get-access-token', '--resource', BksConfig.azure.azSQLLoginScope, '--output', 'json' - ]); + ], { shell: true }); let stdout = ''; let stderr = ''; diff --git a/apps/studio/src/lib/db/clients/utils.ts b/apps/studio/src/lib/db/clients/utils.ts index 6a141415a..f0ea7b457 100644 --- a/apps/studio/src/lib/db/clients/utils.ts +++ b/apps/studio/src/lib/db/clients/utils.ts @@ -6,7 +6,6 @@ import { joinFilters } from '@/common/utils' import { IdentifyResult } from 'sql-query-identifier/lib/defines' import { fromIni } from "@aws-sdk/credential-providers"; import { Signer } from "@aws-sdk/rds-signer"; -import globals from "@/common/globals"; import { AWSCredentials } from "@/lib/db/authentication/amazon-redshift"; @@ -15,6 +14,7 @@ import { AuthOptions } from "@/lib/db/authentication/azure"; import { spawn } from "child_process"; import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader"; import { AwsCredentialIdentity, RuntimeConfigAwsCredentialIdentityProvider } from '@aws-sdk/types' +import platformInfo from '@/common/platform_info' const log = logRaw.scope('db/util') @@ -437,6 +437,18 @@ export async function refreshTokenIfNeeded(iamOptions: IamAuthOptions, server: a return resolvedPw; } +export function sanitizeCommandPath(path: string): string { + if (!path) return path; + + if (platformInfo.isWindows) { + const escaped = path.replace(/"/g, '""'); + return `"${escaped}"`; + } else { + const escaped = path.replace(/'/g, "'\\''"); + return `'${escaped}'`; + } +} + export async function getAWSCLIToken(server: IDbConnectionServerConfig, options: IamAuthOptions): Promise { if (!options?.cliPath) { throw new Error('AZ command not specified'); @@ -449,7 +461,7 @@ export async function getAWSCLIToken(server: IDbConnectionServerConfig, options: } return new Promise((resolve, reject) => { - const proc = spawn(options.cliPath, [ + const proc = spawn(sanitizeCommandPath(options.cliPath), [ 'rds', 'generate-db-auth-token', '--hostname', @@ -461,7 +473,7 @@ export async function getAWSCLIToken(server: IDbConnectionServerConfig, options: '--username', server.user, ...extraArgs - ]); + ], { shell: true }); let stdout = ''; let stderr = ''; diff --git a/apps/studio/src/services/plugin/web/PluginStoreService.ts b/apps/studio/src/services/plugin/web/PluginStoreService.ts index 182a4f5b7..918dcc79c 100644 --- a/apps/studio/src/services/plugin/web/PluginStoreService.ts +++ b/apps/studio/src/services/plugin/web/PluginStoreService.ts @@ -59,7 +59,7 @@ export default class PluginStoreService { } getTheme() { - const styles = getComputedStyle(document.body); + const styles = getComputedStyle(this.getAppEl()); /** Key = css property, value = css value */ const palette: Record = {}; @@ -384,4 +384,8 @@ export default class PluginStoreService { }, }; } + + private getAppEl() { + return document.body.querySelector('.beekeeper-studio-wrapper'); + } } diff --git a/apps/studio/src/services/plugin/web/cssVars.ts b/apps/studio/src/services/plugin/web/cssVars.ts index b8f1f5957..01148f805 100644 --- a/apps/studio/src/services/plugin/web/cssVars.ts +++ b/apps/studio/src/services/plugin/web/cssVars.ts @@ -54,6 +54,7 @@ export const cssVars = [ "--bks-text-editor-error-fg-color", "--bks-text-editor-fg-color", "--bks-text-editor-focused-outline-color", + "--bks-text-editor-font-size", "--bks-text-editor-foldgutter-fg-color", "--bks-text-editor-foldgutter-fg-color-hover", "--bks-text-editor-gutter-bg-color", diff --git a/apps/studio/src/store/modules/CredentialsModule.ts b/apps/studio/src/store/modules/CredentialsModule.ts index f5ec311d7..6cb4c7c8f 100644 --- a/apps/studio/src/store/modules/CredentialsModule.ts +++ b/apps/studio/src/store/modules/CredentialsModule.ts @@ -31,7 +31,7 @@ interface State { async function credentialToBlob(c: TransportCloudCredential): Promise { const clientOptions: CloudClientOptions = { - app: c.appId, email: c.email, token: c.token, baseUrl: window.platformInfo.cloudUrl + app: c.appId, email: c.email, token: c.token, baseUrl: window.platformInfo.cloudUrl, clientVersion: window.platformInfo.appVersion } const client = new CloudClient(clientOptions) try { diff --git a/apps/studio/src/store/modules/PinConnectionModule.ts b/apps/studio/src/store/modules/PinConnectionModule.ts index a18c747f4..9f6d4502e 100644 --- a/apps/studio/src/store/modules/PinConnectionModule.ts +++ b/apps/studio/src/store/modules/PinConnectionModule.ts @@ -62,9 +62,9 @@ export const PinConnectionModule: Module = { return; } - const newPin = await Vue.prototype.$util.send('appdb/pinconn/new', { init: item }); + let newPin = await Vue.prototype.$util.send('appdb/pinconn/new', { init: item }); newPin.position = (context.getters.orderedPins.reverse()[0]?.position || 0) + 1; - await Vue.prototype.$util.send('appdb/pinconn/save', { obj: newPin }) + newPin = await Vue.prototype.$util.send('appdb/pinconn/save', { obj: newPin }) context.commit('add', newPin); }, async reorder(context) { diff --git a/apps/studio/src/store/modules/PinModule.ts b/apps/studio/src/store/modules/PinModule.ts index c5812902d..cd5f8f372 100644 --- a/apps/studio/src/store/modules/PinModule.ts +++ b/apps/studio/src/store/modules/PinModule.ts @@ -7,7 +7,7 @@ import Vue from "vue"; function matches(pin: TransportPinnedEntity, entity: DatabaseEntity, database?: string) { return entity.name === pin.entityName && - ((_.isNil(entity.schema) && _.isNil(pin.schemaName)) || + ((_.isNil(entity.schema) && _.isNil(pin.schemaName)) || entity.schema === pin.schemaName) && entity.entityType === pin.entityType && (!database || database === pin.databaseName) @@ -75,7 +75,7 @@ export const PinModule: Module = { // this used to be !p.hasId(), hopefully this still works? the alternative is ugly const unsavedPins = context.state.pins.filter((p)=> !p.id) await Promise.all(unsavedPins.map((p) => { - p.connectionId === usedConfig.id && p.workspaceId === usedConfig.id && + p.connectionId === usedConfig.id && p.workspaceId === usedConfig.id && Vue.prototype.$util.send('appdb/pins/save', { obj: p }); })) }, @@ -86,7 +86,7 @@ export const PinModule: Module = { if (database && usedConfig) { console.log('GETTING NEW PIN: ', item, database, usedConfig) - const newPin = await Vue.prototype.$util.send('appdb/pins/new', { + let newPin = await Vue.prototype.$util.send('appdb/pins/new', { init: { table: item, db: database, @@ -95,7 +95,9 @@ export const PinModule: Module = { }); console.log('RECEIVED NEW PIN: ', newPin) newPin.position = (context.getters.orderedPins.reverse()[0]?.position || 0) + 1 - if(usedConfig.id) await Vue.prototype.$util.send('appdb/pins/save', { obj: newPin }); + if(usedConfig.id) { + newPin = await Vue.prototype.$util.send('appdb/pins/save', { obj: newPin }); + } context.commit('add', newPin) } }, diff --git a/apps/ui-kit/package.json b/apps/ui-kit/package.json index e72a07d94..5518b33b7 100644 --- a/apps/ui-kit/package.json +++ b/apps/ui-kit/package.json @@ -7,7 +7,7 @@ "url": "https://beekeeperstudio.io" }, "private": false, - "version": "0.3.1", + "version": "0.3.2", "repository": { "type": "git", "url": "git+https://github.com/beekeeper-studio/beekeeper-studio.git", diff --git a/bin/make-ui-kit-release.sh b/bin/make-ui-kit-release.sh new file mode 100755 index 000000000..eb22c38e6 --- /dev/null +++ b/bin/make-ui-kit-release.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +set -euo pipefail + +TAG_PREFIX="ui-kit:v" + +# Function to list the 5 most recent remote ui-kit tags by date +list_recent_remote_tags() { + echo "Fetching the 5 most recent remote ui-kit tags by date:" + git ls-remote --tags origin | \ + grep -E "refs/tags/${TAG_PREFIX}[0-9]+\.[0-9]+\.[0-9]+(-[a-z]+\.[0-9]+)?$" | \ + while read -r hash ref; do + tag="${ref#refs/tags/}" + date=$(git log -1 --format='%ci' "$hash" 2>/dev/null || echo "unknown date") + echo "$tag $date" + done | sort -k2 -r | head -n 5 +} + +# Function to get the next version in sequence +guess_next_version() { + # Get the most recent ui-kit tag + local latest_tag + latest_tag=$(git ls-remote --tags origin | \ + grep -Eo "${TAG_PREFIX}[0-9]+\.[0-9]+\.[0-9]+(-[a-z]+\.[0-9]+)?$" | \ + sed "s/^${TAG_PREFIX}//" | \ + sort -V | tail -n 1) + + if [[ -z "$latest_tag" ]]; then + echo "0.1.0" + return + fi + + if [[ "$latest_tag" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-(alpha|beta)\.([0-9]+))?$ ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + channel="${BASH_REMATCH[5]:-}" + channel_num="${BASH_REMATCH[6]:-}" + + if [[ -n "$channel" ]]; then + # Increment the channel number for pre-releases + channel_num=$((channel_num + 1)) + echo "$major.$minor.$patch-$channel.$channel_num" + else + # Increment the patch version for stable releases + patch=$((patch + 1)) + echo "$major.$minor.$patch" + fi + else + echo "0.1.0" # Default to this if no valid tags exist + fi +} + +# Function to validate version format +validate_version() { + if [[ "$1" =~ ^v?([0-9]+\.[0-9]+\.[0-9]+(-[a-z]+\.[0-9]+)?)$ ]]; then + echo "${BASH_REMATCH[1]}" + else + echo "Error: Invalid version format. Expected x.x.x or x.x.x-channel.x" + exit 1 + fi +} + +# Step 1: List recent tags +list_recent_remote_tags + +# Step 2: Prompt user for new version with a default guess +default_version=$(guess_next_version) +echo "" +read -p "Enter the new version (default: $default_version): " INPUT_VERSION +VERSION="${INPUT_VERSION:-$default_version}" + +# Step 3: Validate and clean the version +VERSION=$(validate_version "$VERSION") +NEW_TAG="${TAG_PREFIX}${VERSION}" + +# Confirm with the user +echo "" +echo "This will:" +echo " - Update apps/ui-kit/package.json to version: $VERSION" +echo " - Push a new git tag: $NEW_TAG" +read -p "Do you want to continue? (y/n): " CONFIRM + +if [[ "$CONFIRM" != "y" ]]; then + echo "Aborted." + exit 0 +fi + +# Step 4: Update package.json if necessary +PACKAGE_VERSION=$(jq -r '.version' apps/ui-kit/package.json) +if [[ "$PACKAGE_VERSION" == "$VERSION" ]]; then + echo "package.json is already updated to version $VERSION." +else + echo "Updating apps/ui-kit/package.json..." + jq ".version = \"$VERSION\"" apps/ui-kit/package.json > apps/ui-kit/package.temp.json + mv apps/ui-kit/package.temp.json apps/ui-kit/package.json + + # Commit changes + echo "Committing changes..." + git add apps/ui-kit/package.json + git commit -m "chore: bump ui-kit version to $VERSION" + git push +fi + +# Step 5: Push tag +if git tag | grep -q "^${NEW_TAG}\$"; then + echo "Tag $NEW_TAG already exists." +else + echo "Creating and pushing tag $NEW_TAG..." + git tag "$NEW_TAG" + git push origin "$NEW_TAG" +fi + +echo "UI Kit release process completed successfully."