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;
-
+
{label}
@@ -33,13 +33,18 @@
- {#if help}
+ {#if $$slots.help || help}
- {help}
+ {#if $$slots.help}
+
+ {/if}
+ {#if help}
+ {help}
+ {/if}
{/if}
-
+
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}
+
+ deleteConnectionModal.open()}
+ danger
+ appearance="default"
+ >
+
+ Disconnect 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}
+
+
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}
-
-
- Create Schema
-
- {/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}
+
+
+ Create Schema
+
+ {/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}
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 @@
+
+
+
diff --git a/mathesar_ui/src/systems/users-and-permissions/UserDetailsForm.svelte b/mathesar_ui/src/systems/users-and-permissions/UserDetailsForm.svelte
index 252544d5d1..9f06fd51fa 100644
--- a/mathesar_ui/src/systems/users-and-permissions/UserDetailsForm.svelte
+++ b/mathesar_ui/src/systems/users-and-permissions/UserDetailsForm.svelte
@@ -21,8 +21,12 @@
} from '@mathesar/components/form';
import { iconSave, iconUndo } from '@mathesar/icons';
import { getUserProfileStoreFromContext } from '@mathesar/stores/userProfile';
+ import GridFormInput from '@mathesar/components/form/GridFormInput.svelte';
+ import { locale, setLocale } from '@mathesar/i18n/i18n-svelte';
+ import { loadLocaleAsync } from '@mathesar/i18n/i18n-load';
+ import { baseLocale } from '@mathesar/i18n/i18n-util';
import SelectUserType from './SelectUserType.svelte';
- import UserFormInput from './UserFormInput.svelte';
+ import SelectDisplayLanguage from './SelectDisplayLanguage.svelte';
const dispatch = createEventDispatcher<{ create: User; update: undefined }>();
const userProfileStore = getUserProfileStoreFromContext();
@@ -41,6 +45,7 @@
),
]);
$: email = optionalField(user?.email ?? '', [isEmail()]);
+ $: displayLanguage = requiredField(user?.display_language ?? baseLocale);
$: userType = requiredField<'user' | 'admin' | undefined>(
user?.is_superuser ? 'admin' : 'user',
);
@@ -49,7 +54,7 @@
$: user, password.reset();
$: formFields = (() => {
- const fields = { fullName, username, email, userType };
+ const fields = { fullName, username, email, userType, displayLanguage };
return isNewUser ? { ...fields, password } : fields;
})();
$: form = makeForm(formFields);
@@ -61,6 +66,7 @@
username: formValues.username,
email: formValues.email,
is_superuser: formValues.userType === 'admin',
+ display_language: formValues.displayLanguage,
};
if (isNewUser && hasProperty(formValues, 'password')) {
@@ -77,6 +83,12 @@
if (isUserUpdatingThemselves && userProfileStore) {
userProfileStore.update((details) => details.with(request));
}
+
+ const updatedLocale = request.display_language;
+ if ($locale !== updatedLocale) {
+ await loadLocaleAsync(updatedLocale);
+ setLocale(updatedLocale);
+ }
dispatch('update');
return;
}
@@ -112,15 +124,15 @@
-
-
+
-
{#if isNewUser}
-
{/if}
-
+
+
+ !('error' in database);
diff --git a/mathesar_ui/src/utils/preloadData.ts b/mathesar_ui/src/utils/preloadData.ts
index 1fbe7e567e..69169dc3b1 100644
--- a/mathesar_ui/src/utils/preloadData.ts
+++ b/mathesar_ui/src/utils/preloadData.ts
@@ -1,7 +1,7 @@
import type {
- Database,
SchemaResponse,
AbstractTypeResponse,
+ Database,
} from '@mathesar/AppTypes';
import type { TableEntry } from '@mathesar/api/types/tables';
import type { QueryInstance } from '@mathesar/api/types/queries';
@@ -18,6 +18,7 @@ export interface CommonData {
user: User;
live_demo_mode: boolean;
current_release_tag_name: string;
+ supported_languages: Record;
is_authenticated: boolean;
routing_context: 'normal' | 'anonymous';
}
diff --git a/mathesar_ui/vite.config.js b/mathesar_ui/vite.config.js
index d9763d0f3a..17d3b5884f 100644
--- a/mathesar_ui/vite.config.js
+++ b/mathesar_ui/vite.config.js
@@ -38,7 +38,11 @@ export default defineConfig({
build: {
manifest: true,
rollupOptions: {
- input: './src/main.ts',
+ input: {
+ main: './src/main.ts',
+ en: './src/i18n/en/index.ts',
+ ja: './src/i18n/ja/index.ts',
+ },
},
outDir: '../mathesar/static/mathesar/',
emptyOutDir: true,