diff --git a/config/context_processors.py b/config/context_processors.py index da5e98a752..14cd4027da 100644 --- a/config/context_processors.py +++ b/config/context_processors.py @@ -1,17 +1,65 @@ from django.conf import settings +from django.templatetags.static import static + from mathesar.utils.frontend import get_manifest_data def frontend_settings(request): + manifest_data = get_manifest_data() + development_mode = settings.MATHESAR_MODE == 'DEVELOPMENT' + + i18n_settings = get_i18n_settings(manifest_data, development_mode) frontend_settings = { - 'development_mode': settings.MATHESAR_MODE == 'DEVELOPMENT', - 'manifest_data': get_manifest_data(), + 'development_mode': development_mode, + 'manifest_data': manifest_data, 'live_demo_mode': getattr(settings, 'MATHESAR_LIVE_DEMO', False), 'live_demo_username': getattr(settings, 'MATHESAR_LIVE_DEMO_USERNAME', None), 'live_demo_password': getattr(settings, 'MATHESAR_LIVE_DEMO_PASSWORD', None), + **i18n_settings } # Only include development URL if we're in development mode. if frontend_settings['development_mode'] is True: frontend_settings['client_dev_url'] = settings.MATHESAR_CLIENT_DEV_URL + return frontend_settings + + +def get_display_language_from_request(request): + # https://docs.djangoproject.com/en/4.2/topics/i18n/translation/#how-django-discovers-language-preference + # This automatically fallbacks to en because of https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-LANGUAGE_CODE + lang_from_locale_middleware = request.LANGUAGE_CODE + + if request.user.is_authenticated: + return request.user.display_language or lang_from_locale_middleware + else: + return lang_from_locale_middleware + + +def get_i18n_settings(manifest_data, development_mode): + """ + Hard coding this for now + but will be taken from users model + and cookies later on + """ + display_language = 'en' + fallback_language = 'en' + + client_dev_url = settings.MATHESAR_CLIENT_DEV_URL + + if development_mode is True: + module_translations_file_path = f'{client_dev_url}/src/i18n/{display_language}/index.ts' + legacy_translations_file_path = f'{client_dev_url}/src/i18n/{display_language}/index.ts' + else: + try: + module_translations_file_path = static(manifest_data[display_language]["file"]) + legacy_translations_file_path = static(manifest_data[f"{display_language}-legacy"]["file"]) + except KeyError: + module_translations_file_path = static(manifest_data[fallback_language]["file"]) + legacy_translations_file_path = static(manifest_data[f"{fallback_language}-legacy"]["file"]) + + return { + 'module_translations_file_path': module_translations_file_path, + 'legacy_translations_file_path': legacy_translations_file_path, + 'display_language': display_language + } diff --git a/config/settings/common_settings.py b/config/settings/common_settings.py index 3889a27cc2..c2d6e52d30 100644 --- a/config/settings/common_settings.py +++ b/config/settings/common_settings.py @@ -15,6 +15,7 @@ from decouple import Csv, config as decouple_config from dj_database_url import parse as db_url +from django.utils.translation import gettext_lazy # We use a 'tuple' with pipes as delimiters as decople naively splits the global @@ -50,6 +51,7 @@ def pipe_delim(pipe_string): "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -254,4 +256,10 @@ def pipe_delim(pipe_string): # List of Template names that contains additional script tags to be added to the base template BASE_TEMPLATE_ADDITIONAL_SCRIPT_TEMPLATES = [] +# i18n +LANGUAGES = [ + ('en', gettext_lazy('English')), + ('ja', gettext_lazy('Japanese')), +] +LANGUAGE_COOKIE_NAME = 'display_language' SALT_KEY = SECRET_KEY diff --git a/mathesar/api/ui/serializers/users.py b/mathesar/api/ui/serializers/users.py index 26fafd0cde..199c439ac4 100644 --- a/mathesar/api/ui/serializers/users.py +++ b/mathesar/api/ui/serializers/users.py @@ -40,6 +40,7 @@ class Meta: 'is_superuser', 'database_roles', 'schema_roles', + 'display_language' ] extra_kwargs = { 'password': {'write_only': True}, diff --git a/mathesar/migrations/0005_user_display_language.py b/mathesar/migrations/0005_user_display_language.py new file mode 100644 index 0000000000..c2eb798493 --- /dev/null +++ b/mathesar/migrations/0005_user_display_language.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2023-09-06 11:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mathesar', '0004_shares'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='display_language', + field=models.CharField(blank=True, default='en', max_length=30), + ), + ] diff --git a/mathesar/migrations/0009_merge_20231025_1733.py b/mathesar/migrations/0009_merge_20231025_1733.py new file mode 100644 index 0000000000..83e79e46ad --- /dev/null +++ b/mathesar/migrations/0009_merge_20231025_1733.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.14 on 2023-10-25 17:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mathesar', '0008_auto_20230921_1834'), + ('mathesar', '0005_user_display_language'), + ] + + operations = [ + ] diff --git a/mathesar/models/users.py b/mathesar/models/users.py index 5bcae308bc..9e3fcbe86a 100644 --- a/mathesar/models/users.py +++ b/mathesar/models/users.py @@ -15,6 +15,7 @@ class User(AbstractUser): full_name = models.CharField(max_length=255, blank=True, null=True) short_name = models.CharField(max_length=255, blank=True, null=True) password_change_needed = models.BooleanField(default=False) + display_language = models.CharField(max_length=30, blank=True, default='en') class Role(models.TextChoices): diff --git a/mathesar/templates/mathesar/index.html b/mathesar/templates/mathesar/index.html index e26130095d..30e8593603 100644 --- a/mathesar/templates/mathesar/index.html +++ b/mathesar/templates/mathesar/index.html @@ -4,9 +4,11 @@ {% block title %}Home{% endblock %} {% block styles %} - {% if not development_mode %} {% for css_file in manifest_data.module_css %} - - {% endfor %} {% endif %} + {% if not development_mode %} + {% for css_file in manifest_data.module_css %} + + {% endfor %} + {% endif %} {% endblock %} {% block scripts %} @@ -17,6 +19,8 @@ {% endfor %} {% endif %} + + {% if development_mode %} @@ -52,12 +56,22 @@ > + + {% endif %} {% endblock %} diff --git a/mathesar/templates/mathesar/login_base.html b/mathesar/templates/mathesar/login_base.html index f631ab17fd..cba3d52333 100644 --- a/mathesar/templates/mathesar/login_base.html +++ b/mathesar/templates/mathesar/login_base.html @@ -77,6 +77,13 @@ font-size: var(--size-xx-large); text-align: center; } + .login-card .language-selector { + display: block; + background-color: transparent; + border: 1px solid var(--slate-200); + border-radius: 0.285rem; + cursor: pointer; + } @media (max-width: 50rem) { .unsupported-device { display: block; @@ -143,5 +150,21 @@

{% block h1 %} {% endblock %}

{% block box_content %} {% endblock %} +
{% endblock %} diff --git a/mathesar/urls.py b/mathesar/urls.py index 62a91e0fe2..0e7379c5fb 100644 --- a/mathesar/urls.py +++ b/mathesar/urls.py @@ -53,10 +53,14 @@ path('administration/users/', views.admin_home, name='admin_users_home'), path('administration/users//', views.admin_home, name='admin_users_edit'), path('administration/update/', views.admin_home, name='admin_update'), + path('administration/db-connection/', views.list_database_connection, name='list_database_connection'), + path('administration/db-connection/add/', views.add_database_connection, name='add_database_connection'), + path('administration/db-connection/edit//', views.edit_database_connection, name='edit_database_connection'), path('shares/tables//', views.shared_table, name='shared_table'), path('shares/explorations//', views.shared_query, name='shared_query'), path('db/', views.home, name='db_home'), path('db//', views.schemas, name='schemas'), + path('i18n/', include('django.conf.urls.i18n')), re_path( r'^db/(?P\w+)/(?P\w+)/', views.schema_home, diff --git a/mathesar/utils/frontend.py b/mathesar/utils/frontend.py index 428017f7f0..8c0ad8fff4 100644 --- a/mathesar/utils/frontend.py +++ b/mathesar/utils/frontend.py @@ -21,11 +21,14 @@ def get_manifest_data(): module_data = raw_data['src/main.ts'] manifest_data['module_css'] = [filename for filename in module_data['css']] manifest_data['module_js'] = module_data['file'] - legacy_data = raw_data['src/main-legacy.ts'] manifest_data['legacy_polyfill_js'] = raw_data['vite/legacy-polyfills-legacy']['file'] manifest_data['legacy_js'] = legacy_data['file'] + for locale, _ in settings.LANGUAGES or []: + manifest_data[locale] = raw_data[f'src/i18n/{locale}/index.ts'] + manifest_data[f"{locale}-legacy"] = raw_data[f'src/i18n/{locale}/index-legacy.ts'] + # Cache data for 1 hour cache.set('manifest_data', manifest_data, 60 * 60) return manifest_data diff --git a/mathesar/views.py b/mathesar/views.py index 758b12813e..c6c8f6a20a 100644 --- a/mathesar/views.py +++ b/mathesar/views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect, get_object_or_404 from rest_framework import status @@ -80,7 +81,12 @@ def get_database_list(request): failed_db_data = [] for db in permission_restricted_failed_db_qs: failed_db_data.append({ + 'id': db.id, + 'username': db.username, + 'port': db.port, + 'host': db.host, 'name': db.name, + 'db_name': db.db_name, 'editable': db.editable, 'error': 'Error connecting to the database' }) @@ -157,6 +163,7 @@ def get_common_data(request, database=None, schema=None): 'databases': get_database_list(request), 'tables': get_table_list(request, schema), 'queries': get_queries_list(request, schema), + 'supported_languages': dict(getattr(settings, 'LANGUAGES', [])), 'routing_context': 'normal', } @@ -301,6 +308,28 @@ def schemas(request, db_name): }) +@login_required +def list_database_connection(request): + return render(request, 'mathesar/index.html', { + 'common_data': get_common_data(request) + }) + + +@login_required +def add_database_connection(request): + return render(request, 'mathesar/index.html', { + 'common_data': get_common_data(request) + }) + + +@login_required +def edit_database_connection(request, db_name): + database = get_current_database(request, db_name) + return render(request, 'mathesar/index.html', { + 'common_data': get_common_data(request, database, None) + }) + + def shared_table(request, slug): shared_table_link = SharedTable.get_by_slug(slug) if is_valid_uuid_v4(slug) else None table = shared_table_link.table if shared_table_link else None diff --git a/mathesar_ui/src/App.svelte b/mathesar_ui/src/App.svelte index 4fe08c214e..f512425f81 100644 --- a/mathesar_ui/src/App.svelte +++ b/mathesar_ui/src/App.svelte @@ -3,38 +3,42 @@ import { preloadCommonData } from '@mathesar/utils/preloadData'; import AppContext from './AppContext.svelte'; import RootRoute from './routes/RootRoute.svelte'; - import { loadLocaleAsync } from './i18n/i18n-load'; import { setLocale } from './i18n/i18n-svelte'; - import type { RequestStatus } from './api/utils/requestUtils'; - import { getErrorMessage } from './utils/errors'; import ErrorBox from './components/message-boxes/ErrorBox.svelte'; + import { loadLocaleAsync, loadTranslations } from './i18n/i18n-load'; + let isTranslationsLoaded = false; /** - * Later the translations file will be loaded - * in parallel to the FE's first chunk + * Why translations are being read from window object? + * In order to - + * 1. Load the translations file in parallel to the first FE chunk. + * 2. And then make it available for the entry(App.svelte) + * file to load them into memory. + * + * The index.html loads it as using a script tag + * Each translations file on load, attaches the translations + * to the window object */ - let translationLoadStatus: RequestStatus = { state: 'processing' }; void (async () => { - try { + const { translations, displayLanguage } = window.Mathesar || {}; + if (translations && displayLanguage) { + loadTranslations(displayLanguage, translations[displayLanguage]); + setLocale(displayLanguage); + isTranslationsLoaded = true; + } else { await loadLocaleAsync('en'); - setLocale('en'); - translationLoadStatus = { state: 'success' }; - } catch (exp) { - translationLoadStatus = { - state: 'failure', - errors: [getErrorMessage(exp)], - }; + isTranslationsLoaded = true; } })(); const commonData = preloadCommonData(); -{#if translationLoadStatus.state === 'success' && commonData} +{#if isTranslationsLoaded && commonData} -{:else if translationLoadStatus.state === 'processing'} +{:else if !isTranslationsLoaded}
diff --git a/mathesar_ui/src/AppTypes.ts b/mathesar_ui/src/AppTypes.ts index d6d46b1fe3..b636863b66 100644 --- a/mathesar_ui/src/AppTypes.ts +++ b/mathesar_ui/src/AppTypes.ts @@ -1,12 +1,28 @@ import type { TreeItem } from '@mathesar-component-library/types'; -export interface Database { +interface BaseDatabase { id: number; name: string; + editable: boolean; + username: string; + host: string; + port: string; + db_name: string; +} + +export interface DatabaseWithConnectionError extends BaseDatabase { + error: string; +} + +export interface SuccessfullyConnectedDatabase extends BaseDatabase { deleted: boolean; supported_types: string[]; } +export type Database = + | SuccessfullyConnectedDatabase + | DatabaseWithConnectionError; + export interface DBObjectEntry { id: number; name: string; diff --git a/mathesar_ui/src/api/databaseConnection.ts b/mathesar_ui/src/api/databaseConnection.ts new file mode 100644 index 0000000000..7fb041e038 --- /dev/null +++ b/mathesar_ui/src/api/databaseConnection.ts @@ -0,0 +1,32 @@ +import type { Database } from '@mathesar/AppTypes'; +import { deleteAPI, patchAPI, postAPI } from './utils/requestUtils'; + +export interface NewConnection { + name: string; + db_name: string; + username: string; + host: string; + port: string; + password: string; +} + +export type ConnectionUpdates = Partial>; + +function add(connectionDetails: NewConnection) { + return postAPI('/api/db/v0/databases/', connectionDetails); +} + +function update(databaseId: number, updates: ConnectionUpdates) { + return patchAPI(`/api/db/v0/databases/${databaseId}/`, updates); +} + +function deleteConnection(databaseId: number, removeMathesarSchemas = false) { + const param = removeMathesarSchemas ? '?del_msar_schemas=True' : ''; + return deleteAPI(`/api/db/v0/databases/${databaseId}/${param}`); +} + +export default { + add, + update, + delete: deleteConnection, +}; diff --git a/mathesar_ui/src/api/users.ts b/mathesar_ui/src/api/users.ts index f90a168396..ad2599105f 100644 --- a/mathesar_ui/src/api/users.ts +++ b/mathesar_ui/src/api/users.ts @@ -1,4 +1,5 @@ import type { Database, SchemaEntry } from '@mathesar/AppTypes'; +import type { Locales } from '@mathesar/i18n/i18n-types'; import { deleteAPI, getAPI, @@ -12,6 +13,7 @@ export interface UnsavedUser { email: string | null; username: string; password: string; + display_language: Locales; } export type UserRole = 'viewer' | 'editor' | 'manager'; diff --git a/mathesar_ui/src/component-library/common/styles/variables.scss b/mathesar_ui/src/component-library/common/styles/variables.scss index d4cd9c0f1b..4befd412a3 100644 --- a/mathesar_ui/src/component-library/common/styles/variables.scss +++ b/mathesar_ui/src/component-library/common/styles/variables.scss @@ -83,6 +83,7 @@ --size-ultra-large: 2.074rem; //2.074rem/33.18px --size-super-ultra-large: 2.488rem; //2.488rem/39.81px + --border-radius-xs: 0.071rem; //1px --border-radius-s: 0.142rem; //2px --border-radius-m: 0.285rem; //4px --border-radius-l: 0.571rem; //8px diff --git a/mathesar_ui/src/pages/admin-users/FormBox.svelte b/mathesar_ui/src/components/form/FormBox.svelte similarity index 100% rename from mathesar_ui/src/pages/admin-users/FormBox.svelte rename to mathesar_ui/src/components/form/FormBox.svelte diff --git a/mathesar_ui/src/systems/users-and-permissions/UserFormInput.svelte b/mathesar_ui/src/components/form/GridFormInput.svelte similarity index 73% rename from mathesar_ui/src/systems/users-and-permissions/UserFormInput.svelte rename to mathesar_ui/src/components/form/GridFormInput.svelte index 19154ed14e..a6e6442be7 100644 --- a/mathesar_ui/src/systems/users-and-permissions/UserFormInput.svelte +++ b/mathesar_ui/src/components/form/GridFormInput.svelte @@ -7,7 +7,7 @@ } from '@mathesar-component-library'; import { Field, type FieldStore } from '@mathesar/components/form'; import type { ComponentWithProps } from '@mathesar-component-library/types'; - import UserFormInputRow from './UserFormInputRow.svelte'; + import GridFormInputRow from './GridFormInputRow.svelte'; const labelController = new LabelController(); $: setLabelControllerInContext(labelController); @@ -22,7 +22,7 @@ export let bypassRow = false; - +
-
+ diff --git a/mathesar_ui/src/systems/users-and-permissions/UserFormInputRow.svelte b/mathesar_ui/src/components/form/GridFormInputRow.svelte similarity index 100% rename from mathesar_ui/src/systems/users-and-permissions/UserFormInputRow.svelte rename to mathesar_ui/src/components/form/GridFormInputRow.svelte diff --git a/mathesar_ui/src/global.d.ts b/mathesar_ui/src/global.d.ts index efcd02a534..307c3e0ddc 100644 --- a/mathesar_ui/src/global.d.ts +++ b/mathesar_ui/src/global.d.ts @@ -5,3 +5,12 @@ declare module '*.mdx' { const value: string; export default value; } + +interface Window { + Mathesar: + | { + displayLanguage: Locales; + translations: Record | undefined; + } + | undefined; +} diff --git a/mathesar_ui/src/i18n/en/index.ts b/mathesar_ui/src/i18n/en/index.ts index 5319277860..e9bc18f660 100644 --- a/mathesar_ui/src/i18n/en/index.ts +++ b/mathesar_ui/src/i18n/en/index.ts @@ -1,4 +1,5 @@ -import type { BaseTranslation } from '../i18n-types.js'; +import type { BaseTranslation, Translations } from '../i18n-types'; +import { addTranslationsToGlobalObject } from '../i18n-util'; const en: BaseTranslation = { general: { @@ -32,3 +33,5 @@ const en: BaseTranslation = { }; export default en; + +addTranslationsToGlobalObject('en', en as Translations); diff --git a/mathesar_ui/src/i18n/formatters.ts b/mathesar_ui/src/i18n/formatters.ts index a6d0c90e49..6da42ded3e 100644 --- a/mathesar_ui/src/i18n/formatters.ts +++ b/mathesar_ui/src/i18n/formatters.ts @@ -1,5 +1,5 @@ import type { FormattersInitializer } from 'typesafe-i18n'; -import type { Locales, Formatters } from './i18n-types.js'; +import type { Locales, Formatters } from './i18n-types'; export const initFormatters: FormattersInitializer< Locales, diff --git a/mathesar_ui/src/i18n/i18n-load.ts b/mathesar_ui/src/i18n/i18n-load.ts index 02f828295e..21fef51b23 100644 --- a/mathesar_ui/src/i18n/i18n-load.ts +++ b/mathesar_ui/src/i18n/i18n-load.ts @@ -1,6 +1,6 @@ -import { initFormatters } from './formatters.js'; -import type { Locales, Translations } from './i18n-types.js'; -import { loadedFormatters, loadedLocales } from './i18n-util.js'; +import { initFormatters } from './formatters'; +import type { Locales, Translations } from './i18n-types'; +import { loadedFormatters, loadedLocales } from './i18n-store'; const localeTranslationLoaders = { ja: () => import('./ja/index.js'), @@ -30,3 +30,8 @@ export async function loadLocaleAsync(locale: Locales): Promise { updateTranslationsDictionary(locale, await importLocaleAsync(locale)); loadFormatters(locale); } + +export function loadTranslations(locale: Locales, translations: Translations) { + updateTranslationsDictionary(locale, translations); + loadFormatters(locale); +} diff --git a/mathesar_ui/src/i18n/i18n-store.ts b/mathesar_ui/src/i18n/i18n-store.ts new file mode 100644 index 0000000000..8f1caebb7b --- /dev/null +++ b/mathesar_ui/src/i18n/i18n-store.ts @@ -0,0 +1,11 @@ +import type { Formatters, Locales, Translations } from './i18n-types'; + +export const loadedLocales: Record = {} as Record< + Locales, + Translations +>; + +export const loadedFormatters: Record = {} as Record< + Locales, + Formatters +>; diff --git a/mathesar_ui/src/i18n/i18n-svelte.ts b/mathesar_ui/src/i18n/i18n-svelte.ts index 77e2f482a9..97a670895f 100644 --- a/mathesar_ui/src/i18n/i18n-svelte.ts +++ b/mathesar_ui/src/i18n/i18n-svelte.ts @@ -4,8 +4,8 @@ import type { Locales, TranslationFunctions, Translations, -} from './i18n-types.js'; -import { loadedFormatters, loadedLocales } from './i18n-util.js'; +} from './i18n-types'; +import { loadedFormatters, loadedLocales } from './i18n-store'; const { locale, LL, setLocale } = initI18nSvelte< Locales, diff --git a/mathesar_ui/src/i18n/i18n-util.ts b/mathesar_ui/src/i18n/i18n-util.ts index d0637bfa55..65f9a475ae 100644 --- a/mathesar_ui/src/i18n/i18n-util.ts +++ b/mathesar_ui/src/i18n/i18n-util.ts @@ -1,14 +1,27 @@ import { initExtendDictionary } from 'typesafe-i18n/utils'; -import type { Formatters, Locales, Translations } from './i18n-types.js'; - -export const loadedLocales: Record = {} as Record< - Locales, - Translations ->; - -export const loadedFormatters: Record = {} as Record< - Locales, - Formatters ->; +import type { BaseLocale, Locales, Translations } from './i18n-types'; export const extendDictionary = initExtendDictionary(); +export const baseLocale: BaseLocale = 'en'; + +export function addTranslationsToGlobalObject( + locale: Locales, + translations: Translations, +) { + /** + * This function is being called by all of the translations files + * The base translation file is being loaded by the typesafe-i18n utility + * to generate types during the development time. + * Hence this function also runs in the context of node + * instead of just browser. + */ + if (typeof window === 'undefined') return; + window.Mathesar = { + ...window.Mathesar, + displayLanguage: locale, + translations: { + ...window.Mathesar?.translations, + [locale]: translations, + }, + }; +} diff --git a/mathesar_ui/src/i18n/ja/index.ts b/mathesar_ui/src/i18n/ja/index.ts index 236baf6231..7c99872b30 100644 --- a/mathesar_ui/src/i18n/ja/index.ts +++ b/mathesar_ui/src/i18n/ja/index.ts @@ -1,7 +1,12 @@ -import en from '../en/index.js'; -import type { Translation } from '../i18n-types.js'; -import { extendDictionary } from '../i18n-util.js'; +import en from '../en/index'; +import type { Translation } from '../i18n-types'; +import { + addTranslationsToGlobalObject, + extendDictionary, +} from '../i18n-util.js'; const ja = extendDictionary(en, {}) as Translation; export default ja; + +addTranslationsToGlobalObject('en', ja); diff --git a/mathesar_ui/src/pages/admin-users/AdminNavigation.svelte b/mathesar_ui/src/pages/admin-users/AdminNavigation.svelte index 1b419287e7..40218f101d 100644 --- a/mathesar_ui/src/pages/admin-users/AdminNavigation.svelte +++ b/mathesar_ui/src/pages/admin-users/AdminNavigation.svelte @@ -1,10 +1,15 @@ + + + +

Add Database Connection

+ + + + diff --git a/mathesar_ui/src/pages/database-connection/DatabaseConnectionForm.svelte b/mathesar_ui/src/pages/database-connection/DatabaseConnectionForm.svelte new file mode 100644 index 0000000000..5993ee788a --- /dev/null +++ b/mathesar_ui/src/pages/database-connection/DatabaseConnectionForm.svelte @@ -0,0 +1,208 @@ + + +
+ + + + + + + + + + + + + The user will need to have CONNECT and CREATE privileges on the database. + Why is this needed?. + + + + +
+ + + diff --git a/mathesar_ui/src/pages/database-connection/DatabaseConnectionItem.svelte b/mathesar_ui/src/pages/database-connection/DatabaseConnectionItem.svelte new file mode 100644 index 0000000000..77de48325a --- /dev/null +++ b/mathesar_ui/src/pages/database-connection/DatabaseConnectionItem.svelte @@ -0,0 +1,80 @@ + + + + {#if !database.editable} + + + + {:else} + + + + {/if} +
+

{database.name}

+ {#if isSuccessfullyConnectedDatabase(database)} +

{database.db_name}

+ {/if} +
+
+ + diff --git a/mathesar_ui/src/pages/database-connection/DatabaseConnectionSkeleton.svelte b/mathesar_ui/src/pages/database-connection/DatabaseConnectionSkeleton.svelte new file mode 100644 index 0000000000..ce0d043ff1 --- /dev/null +++ b/mathesar_ui/src/pages/database-connection/DatabaseConnectionSkeleton.svelte @@ -0,0 +1,16 @@ + + +
+ +
+ + diff --git a/mathesar_ui/src/pages/database-connection/DatabaseConnectionsList.svelte b/mathesar_ui/src/pages/database-connection/DatabaseConnectionsList.svelte new file mode 100644 index 0000000000..ea4e810d49 --- /dev/null +++ b/mathesar_ui/src/pages/database-connection/DatabaseConnectionsList.svelte @@ -0,0 +1,130 @@ + + + + {makeSimplePageTitle('Database Connections')} + + + + +

Database Connections {filteredDatabasesCountText}

+ +
+ {#if databasesLoadStatus === States.Loading} + + {:else if databasesLoadStatus === States.Done} + + + + + Add Database Connection + + + +

+ {labeledCount(filteredDatabases, 'results')} + for all database connections matching {filterQuery} +

+
+ + {#if filteredDatabases.length} +
+ {#each filteredDatabases as db, index (db.id)} + {#if index !== 0} +
+ {/if} + + {/each} +
+ {:else if allDatabases.length === 0} +

No database connection found

+ {/if} +
+
+ {:else if databasesLoadStatus === States.Error} + +

Error: {databasesLoadError}

+
+ {/if} +
+ + diff --git a/mathesar_ui/src/pages/database-connection/EditDatabaseConnection.svelte b/mathesar_ui/src/pages/database-connection/EditDatabaseConnection.svelte new file mode 100644 index 0000000000..b03616f347 --- /dev/null +++ b/mathesar_ui/src/pages/database-connection/EditDatabaseConnection.svelte @@ -0,0 +1,76 @@ + + + + +

Edit Database Connection

+ + + + + +{#if database} + + + + +{/if} diff --git a/mathesar_ui/src/pages/database/ConnectionError.svelte b/mathesar_ui/src/pages/database/ConnectionError.svelte new file mode 100644 index 0000000000..9f3c307a30 --- /dev/null +++ b/mathesar_ui/src/pages/database/ConnectionError.svelte @@ -0,0 +1,39 @@ + + + +

{database.error}

+
+ + + Edit Connection + + +
+
diff --git a/mathesar_ui/src/pages/database/DatabaseDetails.svelte b/mathesar_ui/src/pages/database/DatabaseDetails.svelte index aad6f8c443..96d652dc6e 100644 --- a/mathesar_ui/src/pages/database/DatabaseDetails.svelte +++ b/mathesar_ui/src/pages/database/DatabaseDetails.svelte @@ -15,6 +15,8 @@ iconManageAccess, iconRefresh, iconMoreActions, + iconEdit, + iconDeleteMajor, } from '@mathesar/icons'; import { confirmDelete } from '@mathesar/stores/confirmation'; import { modal } from '@mathesar/stores/modal'; @@ -28,13 +30,21 @@ import { getUserProfileStoreFromContext } from '@mathesar/stores/userProfile'; import { labeledCount } from '@mathesar/utils/languageUtils'; import EntityContainerWithFilterBar from '@mathesar/components/EntityContainerWithFilterBar.svelte'; + import LinkMenuItem from '@mathesar/component-library/menu/LinkMenuItem.svelte'; + import { getDatabaseConnectionEditUrl } from '@mathesar/routes/urls'; + import { reloadDatabases } from '@mathesar/stores/databases'; + import { router } from 'tinro'; + import { isSuccessfullyConnectedDatabase } from '@mathesar/utils/database'; import AddEditSchemaModal from './AddEditSchemaModal.svelte'; import DbAccessControlModal from './DbAccessControlModal.svelte'; import SchemaRow from './SchemaRow.svelte'; import { deleteSchemaConfirmationBody } from './__help__/databaseHelp'; + import ConnectionError from './ConnectionError.svelte'; + import DeleteDatabaseConnectionConfirmationModal from './DeleteDatabaseConnectionConfirmationModal.svelte'; const addEditModal = modal.spawnModalController(); const accessControlModal = modal.spawnModalController(); + const deleteConnectionModal = modal.spawnModalController(); const userProfileStore = getUserProfileStoreFromContext(); $: userProfile = $userProfileStore; @@ -110,6 +120,11 @@ isReflectionRunning = false; } } + + async function handleSuccessfulDeleteConnection() { + await reloadDatabases(); + router.goto('/'); + } + {#if database.editable && userProfile?.isSuperUser} + + Edit Database Connection + + deleteConnectionModal.open()} + > + Disconnect Database + + {/if} {/if} @@ -169,50 +198,61 @@

Schemas ({schemasMap.size})

- - - {#if canExecuteDDL} - - {/if} - -

- {labeledCount(displayList, 'results')} - for all schemas matching - {filterQuery} -

-
    - {#each displayList as schema (schema.id)} -
  • - editSchema(schema)} - on:delete={() => deleteSchema(schema)} - /> -
  • - {/each} -
-
+ {#if !isSuccessfullyConnectedDatabase(database)} + + {:else} + + + {#if canExecuteDDL} + + {/if} + +

+ {labeledCount(displayList, 'results')} + for all schemas matching + {filterQuery} +

+
    + {#each displayList as schema (schema.id)} +
  • + editSchema(schema)} + on:delete={() => deleteSchema(schema)} + /> +
  • + {/each} +
+
+ {/if} - +{#if !('error' in database)} + - + + +{/if} diff --git a/mathesar_ui/src/pages/database/DeleteDatabaseConnectionConfirmationModal.svelte b/mathesar_ui/src/pages/database/DeleteDatabaseConnectionConfirmationModal.svelte new file mode 100644 index 0000000000..24547a3864 --- /dev/null +++ b/mathesar_ui/src/pages/database/DeleteDatabaseConnectionConfirmationModal.svelte @@ -0,0 +1,83 @@ + + + + + + + +

+ Deleting these schemas will also delete any database objects that depend on + them. This should not be an issue if you don't have any data using + Mathesar's custom data types. +

+ +

+ + Learn more about the implications of deleting the + Mathesar schema. +

+ + +
+ + diff --git a/mathesar_ui/src/pages/database/NoDatabaseFound.svelte b/mathesar_ui/src/pages/database/NoDatabaseFound.svelte new file mode 100644 index 0000000000..2be30e167a --- /dev/null +++ b/mathesar_ui/src/pages/database/NoDatabaseFound.svelte @@ -0,0 +1,35 @@ + + + + + + + No Databases Found + + Looks like you don't have any databases set up. You'll need to connect + to a database to start using Mathesar. + + + Add Database Connection + + + + diff --git a/mathesar_ui/src/routes/AdminRoute.svelte b/mathesar_ui/src/routes/AdminRoute.svelte index 39fe9713fd..024260e9da 100644 --- a/mathesar_ui/src/routes/AdminRoute.svelte +++ b/mathesar_ui/src/routes/AdminRoute.svelte @@ -8,8 +8,13 @@ import SoftwareUpdate from '@mathesar/pages/admin-update/SoftwareUpdatePage.svelte'; import AdminNavigation from '@mathesar/pages/admin-users/AdminNavigation.svelte'; import PageLayoutWithSidebar from '@mathesar/layouts/PageLayoutWithSidebar.svelte'; - import { ADMIN_UPDATE_PAGE_URL, ADMIN_URL } from './urls'; + import { + ADMIN_UPDATE_PAGE_URL, + ADMIN_URL, + DATABASE_CONNECTION_SLUG, + } from './urls'; import UsersRoute from './UsersRoute.svelte'; + import DatabaseConnectionRoute from './DatabaseConnectionRoute.svelte'; @@ -53,5 +59,9 @@ + + + + diff --git a/mathesar_ui/src/routes/AuthenticatedRoutes.svelte b/mathesar_ui/src/routes/AuthenticatedRoutes.svelte index a792229898..e71ceec904 100644 --- a/mathesar_ui/src/routes/AuthenticatedRoutes.svelte +++ b/mathesar_ui/src/routes/AuthenticatedRoutes.svelte @@ -1,8 +1,8 @@ + + + + + + + + + + + + diff --git a/mathesar_ui/src/routes/urls.ts b/mathesar_ui/src/routes/urls.ts index 2cd56707eb..e4fb4e285c 100644 --- a/mathesar_ui/src/routes/urls.ts +++ b/mathesar_ui/src/routes/urls.ts @@ -115,6 +115,14 @@ export const ADMIN_USERS_PAGE_URL = `${ADMIN_URL}users/`; export const ADMIN_USERS_PAGE_ADD_NEW_URL = `${ADMIN_URL}users/new/`; export const LOGOUT_URL = '/auth/logout/'; +export const DATABASE_CONNECTION_SLUG = 'db-connection'; +export const DATABASE_CONNECTION_LIST_URL = `${ADMIN_URL}${DATABASE_CONNECTION_SLUG}/`; +export const DATABASE_CONNECTION_ADD_URL = `${ADMIN_URL}${DATABASE_CONNECTION_SLUG}/add/`; + +export function getDatabaseConnectionEditUrl(databaseName: string) { + return `${ADMIN_URL}${DATABASE_CONNECTION_SLUG}/edit/${databaseName}/`; +} + export function getEditUsersPageUrl(userId: number) { return `${ADMIN_USERS_PAGE_URL}${userId}/`; } diff --git a/mathesar_ui/src/stores/databases.ts b/mathesar_ui/src/stores/databases.ts index 649733d8ca..bc524d9de3 100644 --- a/mathesar_ui/src/stores/databases.ts +++ b/mathesar_ui/src/stores/databases.ts @@ -23,7 +23,7 @@ export interface DatabaseStoreData { export const databases = writable({ preload: true, - state: States.Loading, + state: States.Done, data: commonData?.databases ?? [], }); diff --git a/mathesar_ui/src/stores/users.ts b/mathesar_ui/src/stores/users.ts index 73eca377c5..f701007af2 100644 --- a/mathesar_ui/src/stores/users.ts +++ b/mathesar_ui/src/stores/users.ts @@ -17,6 +17,7 @@ import { rolesAllowOperation, type AccessOperation, } from '@mathesar/utils/permissions'; +import { baseLocale } from '@mathesar/i18n/i18n-util'; export class UserModel { readonly id: User['id']; @@ -29,6 +30,8 @@ export class UserModel { readonly username: User['username']; + readonly displayLanguage: User['display_language']; + private databaseRoles: Map; private schemaRoles: Map; @@ -45,6 +48,7 @@ export class UserModel { this.fullName = userDetails.full_name; this.email = userDetails.email; this.username = userDetails.username; + this.displayLanguage = userDetails.display_language; } hasPermission( @@ -119,6 +123,7 @@ export class UserModel { schema_roles: [...this.schemaRoles.values()], full_name: this.fullName, email: this.email, + display_language: this.displayLanguage, }; } @@ -172,6 +177,7 @@ export class AnonymousViewerUserModel extends UserModel { username: 'Anonymous', full_name: 'Anonymous', email: null, + display_language: baseLocale, }); } diff --git a/mathesar_ui/src/systems/users-and-permissions/PasswordChangeForm.svelte b/mathesar_ui/src/systems/users-and-permissions/PasswordChangeForm.svelte index 3ed8d0e98f..575bc1c095 100644 --- a/mathesar_ui/src/systems/users-and-permissions/PasswordChangeForm.svelte +++ b/mathesar_ui/src/systems/users-and-permissions/PasswordChangeForm.svelte @@ -15,8 +15,8 @@ import userApi, { type User } from '@mathesar/api/users'; import { getUserProfileStoreFromContext } from '@mathesar/stores/userProfile'; import WarningBox from '@mathesar/components/message-boxes/WarningBox.svelte'; - import UserFormInput from './UserFormInput.svelte'; - import UserFormInputRow from './UserFormInputRow.svelte'; + import GridFormInputRow from '@mathesar/components/form/GridFormInputRow.svelte'; + import GridFormInput from '@mathesar/components/form/GridFormInput.svelte'; const userProfileStore = getUserProfileStoreFromContext(); $: userProfile = $userProfileStore; @@ -90,8 +90,8 @@
{#if showChangePasswordForm} {#if isUserUpdatingTheirOwnPassword} - - +
- + {/if} - - + - - + {:else} - - +
-
+ {/if}
diff --git a/mathesar_ui/src/systems/users-and-permissions/SelectDisplayLanguage.svelte b/mathesar_ui/src/systems/users-and-permissions/SelectDisplayLanguage.svelte new file mode 100644 index 0000000000..15d75b8e6c --- /dev/null +++ b/mathesar_ui/src/systems/users-and-permissions/SelectDisplayLanguage.svelte @@ -0,0 +1,21 @@ + + +